From 92ce8bce97dee709c793aa32db5a140e927c2b81 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 10 Jul 2026 10:51:04 -0400 Subject: [PATCH 01/14] Document Directus production contract and repair auth --- app/app/api/auth/register/route.ts | 98 +- app/lib/directus.ts | 39 +- directus/README.md | 34 + directus/auth-contract.md | 37 + directus/deployment.md | 54 + directus/env.example | 71 + directus/flows.md | 47 + directus/integration-testing.md | 61 + directus/permissions.md | 90 + directus/reference/deployment-inventory.md | 29 + .../reference/docker-compose.sanitized.yml | 28 + .../reference/environment-variable-names.txt | 24 + directus/reference/exports/access.json | 44 + .../exports/directus-user-fields.json | 19 + directus/reference/exports/flows.json | 35 + directus/reference/exports/operations.json | 94 + directus/reference/exports/permissions.json | 1176 + directus/reference/exports/policies.json | 62 + directus/reference/exports/roles.json | 30 + directus/reference/exports/settings.json | 11 + directus/reference/nginx-summary.txt | 33 + directus/reference/schema-comparison.txt | 8 + .../directus_access-columns.tsv | 5 + .../directus_fields-columns.tsv | 20 + .../directus_flows-columns.tsv | 12 + .../directus_operations-columns.tsv | 12 + .../directus_permissions-columns.tsv | 8 + .../directus_policies-columns.tsv | 8 + .../directus_roles-columns.tsv | 5 + .../directus_settings-columns.tsv | 66 + directus/schema-differences.md | 55 + directus/schema.yaml | 22571 ++++++++++++++++ 32 files changed, 24821 insertions(+), 65 deletions(-) create mode 100644 directus/README.md create mode 100644 directus/auth-contract.md create mode 100644 directus/deployment.md create mode 100644 directus/env.example create mode 100644 directus/flows.md create mode 100644 directus/integration-testing.md create mode 100644 directus/permissions.md create mode 100644 directus/reference/deployment-inventory.md create mode 100644 directus/reference/docker-compose.sanitized.yml create mode 100644 directus/reference/environment-variable-names.txt create mode 100644 directus/reference/exports/access.json create mode 100644 directus/reference/exports/directus-user-fields.json create mode 100644 directus/reference/exports/flows.json create mode 100644 directus/reference/exports/operations.json create mode 100644 directus/reference/exports/permissions.json create mode 100644 directus/reference/exports/policies.json create mode 100644 directus/reference/exports/roles.json create mode 100644 directus/reference/exports/settings.json create mode 100644 directus/reference/nginx-summary.txt create mode 100644 directus/reference/schema-comparison.txt create mode 100644 directus/reference/system-table-columns/directus_access-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_fields-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_flows-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_operations-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_permissions-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_policies-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_roles-columns.tsv create mode 100644 directus/reference/system-table-columns/directus_settings-columns.tsv create mode 100644 directus/schema-differences.md create mode 100644 directus/schema.yaml diff --git a/app/app/api/auth/register/route.ts b/app/app/api/auth/register/route.ts index e9efabe..5cbc79c 100644 --- a/app/app/api/auth/register/route.ts +++ b/app/app/api/auth/register/route.ts @@ -1,13 +1,12 @@ // app/api/auth/register/route.ts import { NextResponse } from "next/server"; import { rateLimit, requestIp } from "@/lib/rate-limit"; +import { + createDirectusUser, + directusUserExists, + loginDirectus, +} from "@/lib/directus"; -const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); -const SERVICE_TOKEN = -process.env.DIRECTUS_SERVICE_TOKEN || -process.env.DIRECTUS_ADMIN_TOKEN || -process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; -const DEFAULT_ROLE = process.env.DIRECTUS_DEFAULT_ROLE || undefined; const SECURE = process.env.NODE_ENV === "production"; function bad(message: string, status = 400) { @@ -15,16 +14,18 @@ function bad(message: string, status = 400) { } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -async function directusLogin(email: string, password: string) { - const r = await fetch(`${DIRECTUS}/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ email, password }), - cache: "no-store", +function isConflict(error: any): boolean { + const code = error?.detail?.errors?.[0]?.extensions?.code; + const message = error?.detail?.errors?.[0]?.message || error?.message || ""; + return error?.status === 409 || code === "RECORD_NOT_UNIQUE" || /unique|already exists/i.test(message); +} + +function logDirectusFailure(stage: string, error: any) { + console.error(`[auth/register] Directus ${stage} failed`, { + status: error?.status, + code: error?.detail?.errors?.[0]?.extensions?.code, + message: error?.detail?.errors?.[0]?.message || error?.message, }); - const j = await r.json().catch(() => ({})); - if (!r.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `Login failed (${r.status})`); - return j?.data || j; } export async function POST(req: Request) { @@ -32,9 +33,6 @@ export async function POST(req: Request) { if (!rateLimit(`register:${requestIp(req)}`, 5, 60 * 60_000)) { return bad("Too many registration attempts. Please try again later.", 429); } - if (!DIRECTUS) return bad("Missing DIRECTUS_URL/NEXT_PUBLIC_API_BASE_URL", 500); - if (!SERVICE_TOKEN) return bad("Missing DIRECTUS_SERVICE_TOKEN / admin token", 500); - const body = await req.json().catch(() => ({} as any)); const email = String(body?.email ?? "").trim().toLowerCase(); const username = String(body?.username ?? "").trim(); @@ -46,53 +44,35 @@ export async function POST(req: Request) { if (password.length < 8) return bad("Password must be at least 8 characters"); if (password !== confirm) return bad("Passwords do not match"); - // Optional pre-check to return a friendly 409 instead of a generic Directus error - const existsRes = await fetch( - `${DIRECTUS}/users?filter[_or][0][email][_eq]=${encodeURIComponent(email)}` + - `&filter[_or][1][username][_eq]=${encodeURIComponent(username)}` + - `&fields=id,email,username&limit=1`, - { - headers: { - Authorization: `Bearer ${SERVICE_TOKEN}`, - Accept: "application/json", - }, - cache: "no-store", + try { + if (await directusUserExists(email, username)) { + return bad("Email or username already in use", 409); } - ); - const existsJson = await existsRes.json().catch(() => ({})); - if (Array.isArray(existsJson?.data) && existsJson.data.length > 0) { - return bad("Email or username already in use", 409); + } catch (error: any) { + logDirectusFailure("duplicate lookup", error); + return bad("Sign-up is temporarily unavailable.", 503); } - // Create user with sane defaults - const createPayload: any = { - email, - username, - password, - }; - if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE; - - const createRes = await fetch(`${DIRECTUS}/users`, { - method: "POST", - headers: { - Authorization: `Bearer ${SERVICE_TOKEN}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(createPayload), - cache: "no-store", - }); - - const cj = await createRes.json().catch(() => ({})); - if (!createRes.ok) { - const msg = cj?.errors?.[0]?.message || cj?.message || `User create failed (${createRes.status})`; - return bad(msg, createRes.status || 500); + let user: { id: string }; + try { + // Resolves the production Users role through the Registration Bot + // policy, then creates an active local-auth account in that role. + user = await createDirectusUser({ email, username, password }); + } catch (error: any) { + if (isConflict(error)) return bad("Email or username already in use", 409); + logDirectusFailure("user creation", error); + return bad("Sign-up is temporarily unavailable.", 503); } - // Auto-login (email-based; directus expects "email" even though it's an identifier) - const tokens = await directusLogin(email, password); + let tokens: any; + try { + tokens = await loginDirectus(email, password); + } catch (error: any) { + logDirectusFailure("post-registration login", error); + return bad("Your account was created, but automatic sign-in failed. Please sign in.", 502); + } - const res = NextResponse.json({ ok: true, id: cj?.data?.id || null }, { status: 201 }); + const res = NextResponse.json({ ok: true, id: user.id }, { status: 201 }); if (tokens?.access_token) { res.cookies.set("ma_at", tokens.access_token, { path: "/", diff --git a/app/lib/directus.ts b/app/lib/directus.ts index f9d705f..950e23c 100644 --- a/app/lib/directus.ts +++ b/app/lib/directus.ts @@ -4,14 +4,28 @@ // ⚠️ NOTE: Do NOT import `headers()` / `cookies()` from "next/headers" here. // Next 15's types can make those async and break build-time type checks in shared libs. -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); -const TOKEN_ADMIN_REGISTER = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; // server-only +const BASE = ( + process.env.DIRECTUS_URL || + process.env.NEXT_PUBLIC_API_BASE_URL || + "" +).replace(/\/$/, ""); +// DIRECTUS_TOKEN_ADMIN_REGISTER is the canonical credential for the production +// Registration Bot policy. Keep the older aliases as fallbacks so an existing +// deployment does not silently lose authentication during migration. +const TOKEN_ADMIN_REGISTER = + process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || + process.env.DIRECTUS_SERVICE_TOKEN || + process.env.DIRECTUS_ADMIN_TOKEN || + ""; // server-only const ROLE_MEMBER_NAME = process.env.DIRECTUS_ROLE_MEMBER_NAME || "Users"; const PROJECTS_COLLECTION = process.env.DIRECTUS_PROJECTS_COLLECTION || "projects"; -if (!BASE) console.warn("[directus] Missing DIRECTUS_URL"); +if (!BASE) console.warn("[directus] Missing DIRECTUS_URL / NEXT_PUBLIC_API_BASE_URL"); if (!TOKEN_ADMIN_REGISTER) - console.warn("[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (used for registration and username login)"); + console.warn( + "[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (or legacy service-token alias) " + + "used for registration and username login" + ); export function bytesFromMB(mb: number) { return Math.round(mb * 1024 * 1024); @@ -119,7 +133,8 @@ export async function dxDELETE(path: string, bearer: string): Promise(path: string, init?: RequestInit): Promise { - if (!TOKEN_ADMIN_REGISTER) throw new Error("Missing DIRECTUS_TOKEN_ADMIN_REGISTER"); + if (!BASE) throw new Error("Missing Directus base URL"); + if (!TOKEN_ADMIN_REGISTER) throw new Error("Missing Directus registration credential"); const res = await fetch(`${BASE}${path}`, { ...init, headers: { @@ -231,13 +246,25 @@ export async function createDirectusUser(input: { } export async function emailForUsername(username: string): Promise { - const q = `/users?filter[username][_eq]=${encodeURIComponent(username)}&fields=email&limit=1`; + const clean = username.trim(); + if (!clean) return null; + const q = `/users?filter[username][_eq]=${encodeURIComponent(clean)}&fields=email&limit=1`; const { data } = await directusAdminFetch<{ data: Array<{ email?: string }> }>(q); const em = data?.[0]?.email; return em ? String(em) : null; } +export async function directusUserExists(email: string, username: string): Promise { + const q = + `/users?filter[_or][0][email][_eq]=${encodeURIComponent(email)}` + + `&filter[_or][1][username][_eq]=${encodeURIComponent(username)}` + + `&fields=id&limit=1`; + const { data } = await directusAdminFetch<{ data: Array<{ id: string }> }>(q); + return Array.isArray(data) && data.length > 0; +} + export async function loginDirectus(email: string, password: string) { + if (!BASE) throw new Error("Missing Directus base URL"); const res = await fetch(`${BASE}/auth/login`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, diff --git a/directus/README.md b/directus/README.md new file mode 100644 index 0000000..0a90703 --- /dev/null +++ b/directus/README.md @@ -0,0 +1,34 @@ +# Directus production reference + +This directory documents how the Next.js application integrates with the production Directus instance. It is evidence for development and review; it is not an automated migration source and must not be applied to production without a separate, reviewed change process. + +## Evidence and authority + +- `schema.yaml` is the fresh schema snapshot collected from production Directus 12.1.1 on 2026-07-10. It is the authoritative schema evidence in this directory for that collection time. +- `reference/` contains immutable, sanitized source evidence collected from the same deployment: deployment inventory, Compose and Nginx summaries, environment-variable names, roles, policies, access assignments, permissions, settings metadata, flows, operations, user-field metadata, and system-table layouts. +- `../app/schema.yaml` is an older Directus 11.12.0 snapshot. It remains useful as historical application evidence, but it is not the current production schema. It must remain in place alongside the current snapshot. +- The Markdown files directly under `directus/` are human-readable interpretations of the application source and raw evidence. When prose conflicts with raw evidence, re-check the evidence and update the prose. +- `env.example` is a names-and-placeholders contract. It contains no production values. + +The exports intentionally exclude users, user-specific access assignments, credentials, sessions, uploaded data, and flow option values. Consequently, a static credential's actual user/role binding and the detailed behavior inside flows cannot be proven from this material. + +## Documents + +- `deployment.md`: production topology and explicitly configured versus unobserved settings. +- `auth-contract.md`: application-to-Directus authentication and account operations. +- `permissions.md`: role, policy, field, filter, validation, and preset matrix. +- `flows.md`: flow and operation graphs, with unresolved behavior called out. +- `schema-differences.md`: structural comparison with `../app/schema.yaml`. +- `integration-testing.md`: proposed disposable integration-test environment. + +## Refresh procedure + +1. Collect from the running production instance without changing it. +2. Sanitize the result before it enters the repository. Exclude secret values, credentials, user rows, email addresses, sessions, cookies, uploaded files, and sensitive flow options. +3. Store a new schema snapshot under `snapshots/`, using a date or version, for example `snapshots/schema-directus-12.1.1-2026-07-10.yaml`. Do not overwrite `schema.yaml` or `../app/schema.yaml` as part of a historical refresh. +4. Store supporting sanitized exports in a correspondingly dated reference location, or clearly record which snapshot they describe. +5. Record the Directus version, database vendor, collection time, and checksums. +6. Compare the new snapshot to the preceding production snapshot and update the human-readable documents only from observed differences. +7. Run a sensitive-data scan and `git diff --check`, then review the complete diff before committing. + +The current collection process is described only by its outputs; the exact export commands are not present. That part of the refresh procedure is therefore **unresolved** and should be documented when a repeatable sanitized collection script is added. diff --git a/directus/auth-contract.md b/directus/auth-contract.md new file mode 100644 index 0000000..19c05f7 --- /dev/null +++ b/directus/auth-contract.md @@ -0,0 +1,37 @@ +# Authentication and account contract + +Status labels mean: + +- **Verified**: directly supported by application source plus the sanitized production exports. +- **Inferred**: the source intends the behavior, but a runtime binding, omitted value, or live test is missing. +- **Unresolved**: the required implementation or evidence is absent. + +The app uses a Directus access token as a user bearer, normally stored in the HTTP-only `ma_at` cookie. It stores a refresh token in HTTP-only `ma_rt` and a five-minute recent-authentication marker in HTTP-only `ma_ra`. In production these cookies are `Secure`, `SameSite=Lax`, and path `/` where set by the current auth routes. + +## Operation matrix + +| Operation | Application route/module | Directus endpoint | Credential and required permission | Success and important failures | Status | +| --- | --- | --- | --- | --- | --- | +| Signup | `POST /api/auth/register`; `app/app/api/auth/register/route.ts` | `GET /users` duplicate lookup; `GET /roles` Users-role lookup; `POST /users`; then `POST /auth/login` | The canonical static server credential is `DIRECTUS_TOKEN_ADMIN_REGISTER`, with legacy service/admin names as fallbacks. It must represent the Registration Bot role/policy or an equivalent identity. Registration policy grants the required role read and user read/create permissions. | Validates all fields, email shape, password length >=8, and confirmation. Resolves the `Users` role, creates an active account with email/username/password, and performs email-based auto-login. Successful login sets `ma_at` and possibly `ma_rt`. Permission/configuration failures are logged server-side and returned as generic temporary-unavailable responses. | Application flow, build, mocked request contract, and policy are **verified**; production static credential-to-role binding still requires deployment verification because users/values were omitted. | +| Duplicate-user check | Same route | `GET /users?filter[...]&fields=id&limit=1` | Registration policy requires `directus_users.read` for `id`, which is exported. | A returned row or create-time uniqueness conflict produces the same generic email-or-username 409. A failed lookup stops signup with a generic 503 instead of continuing with uncertain permissions. | **Verified** source and mocked request behavior; production credential capability requires a live test. | +| Email login | `POST /api/auth/login`; `app/app/api/auth/login/route.ts` | `POST /auth/login` | No bearer is supplied; local email/password credentials are sent to Directus. No collection permission is used for the auth endpoint itself. | On success sets one-hour `ma_at`, optional 30-day `ma_rt`, and optionally five-minute `ma_ra` for a reauth request. Authentication failures are deliberately collapsed to a generic 401. App IP limiter allows 20 attempts/minute per process. | **Verified** implementation; live Directus local-auth behavior is **inferred** pending integration tests. | +| Username login | Same login route; `emailForUsername()` in `app/lib/directus.ts` | Static-token `GET /users` lookup, then `POST /auth/login` with resolved email | The same registration credential used by signup must read `directus_users.username,email`; Registration policy exports those fields. | Missing username gives generic 401. Lookup errors give 503. Resolved email follows email-login behavior. The shared helper accepts `DIRECTUS_URL` with `NEXT_PUBLIC_API_BASE_URL` fallback. | Source, build, required policy, and mocked request contract are **verified**; credential binding and end-to-end production success require a deployment test. | +| Current user | `GET /api/account`, `GET /api/me`, proxy `/api/dx/users/me`, middleware; relevant modules under `app/app/api/` and `app/middleware.ts` | `GET /users/me` with varying field lists | Current user's access token. Users Self Access policy permits own-row read of `id,username,first_name,last_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications`; Users policy additionally permits public-like `id,username,first_name,avatar,location`. | Returns requested profile fields if field permissions allow. Middleware checks `id` at most once per minute and redirects to reauth on failure. `/api/me` requests `display_name`, which is not present in the exported custom user-field metadata or allowed-field lists, so that field's behavior is unresolved. | Own-row permissions and route calls are **verified**. | +| Token refresh | Token storage occurs in register/login and `app/lib/auth-cookies.ts` | Expected Directus endpoint would be `/auth/refresh`, but no call exists | `ma_rt` is stored, but no application route consumes it. | There is no implemented refresh behavior. Expired/invalid `ma_at` causes middleware to clear `ma_at`/`ma_v` and redirect to sign-in. | **Verified missing implementation**. | +| Logout | `POST` or `GET /api/auth/logout`; `app/app/api/auth/logout/route.ts` | None | No Directus call and no credential required by the handler. | Expires only `ma_at`. It does not revoke the Directus session and does not clear `ma_rt`, `ma_ra`, or `ma_v`. | **Verified**. | +| Password change | `POST`/`PATCH /api/account/password`; `app/app/api/account/password/route.ts` | `GET /users/me`; optionally static-token `GET /users`; `POST /auth/login`; `PATCH /users/me` | Current user token needs own-row read and `directus_users.update` for `password`; Users Self Access grants both. Username verification additionally needs Registration read permission. | Requires current/new values and an eight-character new password. Verifies the current password through local login, then patches only `password`. External-provider rejection depends on reading `provider`, but `provider` is not in the Users Self Access read fields, so this guard may not receive the field. Successful change does not rotate or clear existing app tokens. | Main path and permissions **verified**; provider guard and session effects **unresolved**. | +| Profile changes | `PATCH /api/account` and `PATCH /api/account/profile`; UI primarily uses the latter | `PATCH /users/me` | Current user token. Users Self Access update permits `first_name,last_name,email,avatar,location,password`; it does not permit `username`. | First/last name and location update directly. Email requires `ma_ra=1`; the marker is cleared after a successful sensitive update. UI always includes email in its payload, so its normal save path requests reauthentication even when email is unchanged. Username is classified sensitive but is never added to the patch payload and is not update-permitted. | **Verified**. | +| Avatar change | `POST /api/account/avatar`; `app/app/api/account/avatar/route.ts` | `POST /files`, then `PATCH /users/me` | Current user token. Users policy permits `directus_files.create` on all fields; Users Self Access permits own `avatar` update. | Requires a configured avatar folder, <=10 MB JPEG/PNG/WebP/GIF, and recognized signature. Uploads file then links its ID. If linking fails, the uploaded file is not cleaned up. | **Verified** except folder value and folder-specific enforcement, which are **unresolved**. | +| Reauthentication | `POST /api/auth/reconfirm`; `app/app/api/auth/reconfirm/route.ts`, or login with `reauth=true` | Optional static-token `GET /users`, then `POST /auth/login` | Local credentials; username form also requires Registration `directus_users.read`. | Successful reconfirm replaces `ma_at` and sets `ma_ra` for five minutes. It does not set/replace `ma_rt`. Login's reauth path sets `ma_ra` alongside normal token cookies. Failures are generic 401. | **Verified** implementation; end-to-end username path remains unresolved. | + +## Permission identity model + +The export verifies these role-to-policy assignments: + +- Registration Bot role -> Registration policy. +- Users role -> Users Self Access policy and Users policy, in that sort order. +- Administrator role -> Administrator policy. +- Supporter Bot role -> Supporter policy. +- Public policy has a role-less access assignment. + +Because user rows and user-specific access assignments were intentionally omitted, the evidence cannot prove which identity owns any configured static credential. A deployment test must verify each credential's effective policy rather than relying on its environment-variable name. diff --git a/directus/deployment.md b/directus/deployment.md new file mode 100644 index 0000000..085606f --- /dev/null +++ b/directus/deployment.md @@ -0,0 +1,54 @@ +# Production deployment + +Unless marked **inferred** or **unresolved**, facts below are verified by `reference/deployment-inventory.md`, `reference/docker-compose.sanitized.yml`, `reference/environment-variable-names.txt`, `reference/nginx-summary.txt`, or `reference/exports/settings.json`. + +## Runtime and topology + +| Item | Production observation | +| --- | --- | +| Directus version | 12.1.1 | +| Container image | `directus/directus:latest` | +| Compose service and container | `directus` | +| Restart policy | `unless-stopped` | +| Database client | MySQL; host, database, and credential values were intentionally omitted | +| Public URL | `https://forms.lasereverything.net` | +| Container port | Directus listens on 8055; Compose binds it to host loopback `127.0.0.1:8056` | +| Networks | Private `directus_net` bridge with IPv4/IPv6 subnets, plus external `backend_net` | +| Upload mount | Host `./uploads` to `/directus/uploads`, read/write | +| Extension mount | Host `./extensions` to `/directus/extensions`, read/write | + +Production currently uses the mutable `directus/directus:latest` image tag. This document records that fact and does not change it. The running version was separately observed as 12.1.1, so a future container recreation could select a different image unless deployment controls outside the supplied evidence pin it. + +The extensions directory is mounted, but its contents were not exported. Whether custom extensions are installed or active is **unresolved**. + +## Reverse proxy + +Nginx terminates TLS for `forms.lasereverything.net` and proxies to host loopback port 8056. It forwards the original host, client IP, forwarding chain, scheme, and WebSocket upgrade headers. Proxy read and send timeouts are 300 seconds. The request body limit is 100 MB. TLS 1.2 and 1.3 are enabled and HSTS is emitted for one year with subdomains. + +The sanitized Nginx summary shows reflected-origin CORS response headers with credentials enabled and permits the common API methods and request headers. Directus-specific CORS environment variables are not explicitly set. The exact Nginx origin allow-list or surrounding condition that governs the reflected origin was not included, so whether arbitrary origins are accepted is **unresolved**. + +## Mail + +Directus is explicitly configured for SMTP transport. The observed categories are sender, SMTP host, port, secure mode, username, and password. Port 465 and secure mode `true` are verified; sender, host, and credential values were omitted. Delivery behavior was not tested. + +The Next.js application separately references its own SMTP variable names for supporter-claim mail. Those names are documented in `env.example`; whether they point to the same SMTP service is **unresolved**. + +## Authentication and security configuration + +| Area | Explicit production evidence | Default or unresolved behavior | +| --- | --- | --- | +| Bootstrap administrator | `ADMIN_EMAIL` and `ADMIN_PASSWORD` names are present | Values and whether bootstrap variables still affect an initialized database are intentionally unknown | +| Signing/encryption material | `KEY` and `SECRET` names are present | Values omitted | +| Public registration | No registration environment variables are explicit; settings export has `public_registration=0` | Public `/users/register` is disabled; the app instead uses a privileged `/users` create flow | +| Registration verification | Settings export has `public_registration_verify_email=0` and no public registration role/filter | Inactive while public registration is disabled | +| Auth providers | `AUTH_PROVIDERS` is not explicitly set | Available/default providers were not exported; application code assumes local email/password auth | +| Login-attempt setting | Column layout reports a database default of 25 | The live `auth_login_attempts` value was not exported; the app also applies an in-memory 20-per-minute IP limit | +| Password policy | Column exists and defaults to null in the exported table layout | The live setting was not exported; the app enforces only an eight-character minimum on registration/change | +| CORS | No Directus CORS environment variables are explicit | Nginx adds CORS headers; effective origin policy is unresolved as noted above | +| Cookies | No Directus cookie environment variables are explicit | The app stores Directus tokens in its own `ma_at` and `ma_rt` cookies; effective Directus defaults are not proven here | +| TFA | All exported policies have `enforce_tfa=0` | Per-user TFA state was intentionally not exported | +| Policy IP restrictions | All exported policies have `ip_access=null` | No policy-level IP restriction is evidenced | +| Admin/app access | Administrator policy has both; all other exported policies have neither | Verified from policy export | +| MCP/AI/new 12.x settings | System-table columns exist | Live values were not included in the sanitized settings export and must not be inferred from column defaults | + +Environment absence proves only that a setting was not explicitly supplied to the collected container. It does not prove behavior controlled by the database, an extension, another configuration source, or a Directus default that may vary by version. diff --git a/directus/env.example b/directus/env.example new file mode 100644 index 0000000..14449cf --- /dev/null +++ b/directus/env.example @@ -0,0 +1,71 @@ +# Names-and-placeholders contract only. Never place production values in this file. +# Classification: public-build = embedded/exposed by Next.js; server = Next.js server only; +# deployment = Directus container/bootstrap only. + +# Shared Directus location +NEXT_PUBLIC_API_BASE_URL= # Next.js; public-build +NEXT_PUBLIC_ASSET_URL= # Next.js; public-build +DIRECTUS_URL= # Next.js; server + +# Next.js privileged Directus identities +DIRECTUS_TOKEN_ADMIN_REGISTER= # Next.js; server +DIRECTUS_SERVICE_TOKEN= # Next.js; server +DIRECTUS_ADMIN_TOKEN= # Next.js; server +DIRECTUS_TOKEN_ADMIN_SUPPORTER= # Next.js and backfill script; server +DIRECTUS_TOKEN_SUBMIT= # Next.js; server + +# Next.js Directus role, collection, folder, and upload configuration +DIRECTUS_ROLE_MEMBER_NAME= # Next.js helper; server +DIRECTUS_PROJECTS_COLLECTION= # Next.js helper; server +DIRECTUS_AVATAR_FOLDER_ID= # Next.js; server +DX_FOLDER_FIBER_PHOTOS= # Next.js; server +DX_FOLDER_FIBER_SCREENS= # Next.js; server +DX_FOLDER_GALVO_PHOTOS= # Next.js; server +DX_FOLDER_GALVO_SCREENS= # Next.js; server +DX_FOLDER_GANTRY_PHOTOS= # Next.js; server +DX_FOLDER_GANTRY_SCREENS= # Next.js; server +DX_FOLDER_UV_PHOTOS= # Next.js; server +DX_FOLDER_UV_SCREENS= # Next.js; server +FILE_MAX_MB= # Next.js project uploads; server +SETTINGS_IMAGE_MAX_MB= # Next.js settings uploads; server + +# Directus container identity and cryptographic configuration +KEY= # Directus; deployment +SECRET= # Directus; deployment +PUBLIC_URL= # Directus; deployment +ADMIN_EMAIL= # Directus bootstrap; deployment +ADMIN_PASSWORD= # Directus bootstrap; deployment + +# Directus database +DB_CLIENT=mysql # Directus; deployment +DB_HOST= # Directus; deployment +DB_PORT= # Directus; deployment +DB_DATABASE= # Directus; deployment +DB_USER= # Directus; deployment +DB_PASSWORD= # Directus; deployment +DB_FILENAME= # Directus environment name observed; deployment + +# Directus mail +EMAIL_TRANSPORT=smtp # Directus; deployment +EMAIL_FROM= # Directus and Next.js supporter mail; deployment/server +EMAIL_SMTP_HOST= # Directus; deployment +EMAIL_SMTP_PORT=465 # Directus; deployment +EMAIL_SMTP_SECURE=true # Directus; deployment +EMAIL_SMTP_USER= # Directus; deployment +EMAIL_SMTP_PASSWORD= # Directus; deployment + +# Next.js supporter mail uses a separate naming convention +SMTP_HOST= # Next.js; server +SMTP_PORT= # Next.js; server +SMTP_SECURE= # Next.js; server +SMTP_USER= # Next.js; server +SMTP_PASS= # Next.js; server + +# Runtime classification +NODE_ENV= # Directus and Next.js; deployment/server + +# The production Directus environment also contains runtime-provided NODE_VERSION, +# NPM_CONFIG_UPDATE_NOTIFIER, PATH, and YARN_VERSION. They are not application +# integration configuration and therefore have no configurable placeholders here. +# AUTH_PROVIDERS, Directus CORS/cookie variables, and public-registration variables +# were explicitly reported as not set and are intentionally not implied here. diff --git a/directus/flows.md b/directus/flows.md new file mode 100644 index 0000000..251e18f --- /dev/null +++ b/directus/flows.md @@ -0,0 +1,47 @@ +# Directus flows + +Two active event-triggered flows were exported. Both run with accountability `all`. The export deliberately replaces every operation's options with only `options_keys`; code, filters, collections, queries, keys, error messages, status codes, and event scopes are not available. The graphs below are therefore structural, not behavioral proofs. + +## `unique_key_claims` + +Description: “A flow for setting a unique key for each claim to prevent duplicates.” + +```text +Run Script (exec_5dli5) + resolve -> Read Data (item_read_e4myo) + resolve -> Condition (condition_s458i) + resolve -> Throw Error (throw_error_22tpm) + reject -> end + reject -> end + reject -> end +``` + +- Trigger: event. +- Flow option keys: `type`, `scope`, `collections`, `return`. +- Script option key: `code`. +- Read option keys: `permissions`, `emitEvents`, `collection`, `query`. +- Condition option key: `filter`. +- Error option keys: `status`, `message`, `code`. + +**Unresolved:** the triggering collection/event, how the unique key is calculated, what data is queried, the condition's polarity, error response, and whether the graph handles races atomically. The description suggests duplicate prevention, but omitted options prevent verification. + +## `claim_auto_assign` + +Description: “Assigns a claimant to owner and rejects other pending claims.” + +```text +Condition (condition_dl7cj) + resolve -> Read Data (item_read_oyges) -> end + reject -> end +``` + +- Trigger: event. +- Flow option keys: `type`, `scope`, `collections`. +- Condition option key: `filter`. +- Read option keys: `permissions`, `emitEvents`, `collection`, `key`. + +**Unresolved:** the triggering collection/event, condition, item key, read collection, and any mutation that performs the assignment or rejection. The exported graph contains only a condition followed by an item-read operation; no update operation is visible. The description's assignment behavior therefore cannot be reconciled with the sanitized graph without the omitted options or additional extension/hook behavior. + +## Refreshing flow documentation + +A future sanitized export should retain non-sensitive structural option values such as event type, collection name, and graph routing when safe, while continuing to remove credentials, destination addresses, headers, code containing secrets, and personal data. Each retained value must be reviewed rather than copied wholesale. diff --git a/directus/integration-testing.md b/directus/integration-testing.md new file mode 100644 index 0000000..72c2feb --- /dev/null +++ b/directus/integration-testing.md @@ -0,0 +1,61 @@ +# Proposed disposable Directus integration testing + +No tests described here have been implemented or run. This is a future design that must never target production. + +## Environment + +Use a dedicated Compose project with: + +- A version-pinned Directus image matching production (currently 12.1.1), not the production `latest` deployment reference. +- A disposable MySQL database matching the production database family. +- Ephemeral uploads and a test-only extensions mount populated only when production extensions have been reviewed and are needed. +- Random per-run signing secrets and test-only static credentials. +- A loopback-only port and an unmistakable test public URL. +- Mail captured by a local sink; no external delivery. +- No production database, network, credentials, host mounts, or URLs. + +Provision schema and access configuration from reviewed test fixtures derived from `schema.yaml` and the sanitized role/policy exports. Do not apply the production snapshot automatically until a reviewed fixture process accounts for system collections and version compatibility. + +## Test identities + +Create only synthetic records: + +- Registration Bot with Registration policy and a test-only static credential. +- Supporter Bot with Supporter policy if cross-feature checks need it. +- A normal Users-role account with both Users policies. +- An administrator used only for fixture setup/teardown, never by application requests. + +Have the test harness assert each identity's effective permissions before auth scenarios. This catches a valid credential attached to the wrong role/policy. + +## Required scenarios + +| Area | Cases and assertions | +| --- | --- | +| Registration | Valid account creates an active, login-capable Users-role account; app cookies have expected flags; omitted/default role behavior is explicit; whitespace in passwords is preserved. | +| Duplicate accounts | Duplicate email and duplicate username return the same non-enumerating conflict; mixed case behavior is recorded; failed pre-check cannot turn into a permission disclosure. | +| Email login | Correct credentials succeed; incorrect password and unknown account have indistinguishable responses; rate-limit behavior is isolated per test process. | +| Username login | Registration credential resolves username using only permitted fields; correct credentials succeed; unknown username is generic; missing/mis-scoped credential fails clearly without exposing Directus internals. | +| Current user | `/users/me` and application wrappers return only expected fields; another user's private fields cannot be queried; middleware validation accepts a valid token and rejects expired/revoked tokens. | +| Refresh | Once implemented, valid refresh rotates/returns expected tokens and cookies; expired/revoked/reused tokens fail; the endpoint never accepts an access token in place of refresh. Until then, assert/document that no application refresh endpoint exists. | +| Logout | Once strengthened, clears all auth state and invokes Directus revocation as designed. Until then, characterize the current local-only `ma_at` clearing and retained refresh cookie as a known failing contract. | +| Profile changes | Own first/last name and location succeed; email requires recent reauth; username update is denied; other-user updates are denied; the recent-auth marker expires and is single-use. | +| Password changes | Correct current password succeeds; incorrect current password fails; minimum length is enforced; username identifier path works; external-provider behavior is tested if enabled; existing token/session behavior is explicitly asserted. | +| Avatar changes | Allowed image succeeds into the test folder and links to self; bad type/signature/size fails; other-user linking and unauthorized folder behavior fail; partial upload cleanup behavior is characterized. | +| Least privilege | Registration identity can read only required role/user fields and create only permitted user fields; cannot update/delete users, read unrelated collections, administer settings, or access the Data Studio. Test whether exported sensitive read fields can be narrowed. | + +## Permission-focused negative tests + +- Remove Registration `directus_users.read`: username resolution and duplicate checks should fail predictably while the effect on create is separately asserted. +- Remove Registration `directus_users.create`: signup must fail without leaking permission internals. +- Attach the static credential to the wrong role to prove deployment validation catches it. +- Attempt to override role, status, policy, ID, and authentication data through the public app payload; the server must ignore or reject every override. +- Verify Users updates are limited to `id=$CURRENT_USER` and permitted fields. +- Exercise the exported `user_rigs` `%CURRENT_USER` update filter and record actual Directus 12.1.1 behavior. + +## Harness and lifecycle + +Run tests from outside the Next.js process against its HTTP interface, with a smaller Directus-level suite for permission diagnostics. Wait for both MySQL and Directus health, seed fixtures idempotently, run serial auth tests where rate limiting or cookie state matters, then destroy containers and volumes. + +Log method, path template, status, Directus error code, and synthetic fixture ID only. Redact credentials and cookie values at the logging boundary. Never snapshot response bodies containing private profile fields. + +CI should fail if the configured Directus version differs from the fixture version, a required permission assertion changes, a sensitive environment name lacks a test placeholder, or teardown leaves the disposable project running. diff --git a/directus/permissions.md b/directus/permissions.md new file mode 100644 index 0000000..84321b1 --- /dev/null +++ b/directus/permissions.md @@ -0,0 +1,90 @@ +# Roles, policies, and permissions + +This is a readable rendering of `reference/exports/roles.json`, `policies.json`, `access.json`, and `permissions.json`. `C/R/U/D` mean create/read/update/delete. An absent action means no permission row was exported; it does not establish permissions inherited from an omitted user-specific policy. + +## Role and policy assignments + +| Role/access subject | Assigned policy | Admin access | Data Studio app access | Enforced TFA | IP restriction | +| --- | --- | --- | --- | --- | --- | +| Administrator | Administrator | Yes | Yes | No | None | +| Registration Bot | Registration | No | No | No | None | +| Supporter Bot | Supporter | No | No | No | None | +| Users | Users Self Access (sort 1), Users (sort 2) | No | No | No | None | +| Public/role-less | Public | No | No | No | None | + +No user-specific access assignments appear in the sanitized export because user data was intentionally omitted. The Administrator policy's collection permissions are implicit in `admin_access`; no individual rows were exported for it. + +## High-level matrix + +| Collection group | Public | Users | Users Self Access | Registration | Supporter | +| --- | --- | --- | --- | --- | --- | +| Public catalog/content collections listed below | R | R | - | - | - | +| `directus_fields` | R: `id,collection,field` | R: metadata fields listed below | - | - | - | +| `directus_files` | R all fields | C/R all fields | - | - | - | +| `directus_users` | - | R public profile fields | own-row R/U | R/C broad registration fields | - | +| Projects and settings collections | R | C/R/U/D | - | - | - | +| `projects_files` | R | C/R | - | - | - | +| `user_claims` | - | C/R/U/D with presets and own/pending filters | - | - | - | +| `user_rigs` | - | C/R/U/D with owner controls | - | - | - | +| `user_rig_type` | - | C/R | - | - | - | +| `user_memberships` | - | - | - | - | C/R/U all fields | +| `directus_roles` | - | - | - | R selected role fields | - | + +## Public policy + +Unrestricted read, all fields, is exported for: + +`bg_cat`, `bg_entries`, `bg_sub_cat`, `directus_files`, `hazard_danger`, `hazard_severity`, `hazard_source`, `hazard_tags`, `laser_focus_lens`, `laser_focus_lens_config`, `laser_focus_lens_diameter`, `laser_scan_lens`, `laser_scan_lens_apt`, `laser_scan_lens_config`, `laser_scan_lens_exp`, `laser_software`, `laser_source`, `material`, `material_cat`, `material_coating`, `material_coating_hazard_tags`, `material_color`, `material_hazard_tags`, `material_opacity`, `material_status`, `projects`, `projects_files`, `settings_co2gal`, `settings_co2gan`, `settings_fiber`, and `settings_uv`. + +Public can also read `directus_fields`, restricted to `field,collection,id`. There are no exported public create, update, or delete permissions. + +## Users policy + +Users have unrestricted, all-field read for every public content collection above. Additional or expanded permissions are: + +| Collection | Actions | Row filter | Fields/presets | +| --- | --- | --- | --- | +| `directus_fields` | R | None | `id,collection,field,special,interface,options,readonly,hidden,required,sort,width,group,note` | +| `directus_files` | C/R | None | All fields; no folder/owner filter or preset exported | +| `directus_users` | R | None | `username,first_name,avatar,location,id` | +| `projects` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | All fields; C presets `owner` and `uploader` to `$CURRENT_USER` | +| `projects_files` | C/R | None | All fields; no ownership filter exported | +| `settings_co2gal` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | All fields; C presets owner/uploader to current user | +| `settings_co2gan` | C/R/U/D | Same as above | Same as above | +| `settings_fiber` | C/R/U/D | Same as above | Same as above | +| `settings_uv` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | C/R/U all fields and owner/uploader presets on C; D has `fields=null` in the export | +| `user_claims` | C/R/U/D | R: claimant is current user. U/D: claimant is current user and status is `pending`. C unrestricted. | C fields omit `claimant` and `status` but preset them to current user/`pending`; R selected fields; U only `note`; D `fields=null` | +| `user_rigs` | C/R/U/D | R/D require owner current user. U filter contains `owner _eq "%CURRENT_USER"` exactly as exported. C unrestricted. | All fields; C presets owner to `$CURRENT_USER` | +| `user_rig_type` | C/R | None | All fields | + +The `%CURRENT_USER` update filter on `user_rigs` differs from the `$CURRENT_USER` token used everywhere else. This is a **verified exported value** and is likely ineffective, but its Directus runtime interpretation has not been tested. + +No validation rules are present on any exported Users permission row. + +## Users Self Access policy + +Both rows require `id=$CURRENT_USER`: + +- Read fields: `username,last_name,first_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications,id`. +- Update fields: `first_name,last_name,email,password,avatar,location`. + +There are no presets or validation rules. Username is readable but not updateable. The exported read list includes sensitive field names such as `password` and `tfa_secret`; Directus may suppress stored secret values independently, but that behavior is **not proven by this export** and should be tested. + +## Registration policy + +- `directus_users.create`: unrestricted rows; fields `email,username,password,role,status,auth_data,first_name,last_name,avatar,id`. +- `directus_users.read`: unrestricted rows; a broad field list including `username,email,id,password,auth_data,token,policies,role,status,tfa_secret` and UI/theme fields. +- `directus_roles.read`: unrestricted rows; fields `id,name,icon,description,parent,children`. + +No validation rules or presets are exported. Least privilege therefore depends on the static credential being held only by the server and the application strictly controlling submitted fields. The application registration route currently uses email, username, password, and optionally role; username-to-email login resolution uses read access. + +## Supporter policy + +`user_memberships` has unrestricted create, read, and update permissions on all fields. No delete row, row filters, validation rules, or presets are exported. The policy is assigned to Supporter Bot. + +## Validation and preset summary + +- Every exported `validation` value is null. +- Presets exist only for Users creates on projects/settings (`owner`/`uploader`), user claims (`claimant`/`pending`), and user rigs (`owner`). +- Row filters exist only for Users updates/deletes and selected reads described above, plus own-row filters in Users Self Access. +- Null permissions mean no row filter in these exports; null fields on delete rows are preserved as observed rather than interpreted as all/no fields. diff --git a/directus/reference/deployment-inventory.md b/directus/reference/deployment-inventory.md new file mode 100644 index 0000000..68dfabd --- /dev/null +++ b/directus/reference/deployment-inventory.md @@ -0,0 +1,29 @@ +# Directus Deployment Inventory + +Generated from the running production container. + +- Directus version: 12.1.1 +- Container image: directus/directus:latest +- Public URL: https://forms.lasereverything.net +- Host binding: 127.0.0.1:8056 +- Database client: mysql +- Database service and database names: intentionally omitted +- Email transport: smtp +- SMTP port: 465 +- SMTP secure mode: true +- Explicit AUTH_PROVIDERS variable: not set +- Explicit CORS variables: not set +- Explicit cookie variables: not set +- Explicit registration environment variables: not set + +## Persistent mounts + +- `/directus/extensions` is mounted from the host, mode `rw` +- `/directus/uploads` is mounted from the host, mode `rw` + +## Notes + +- Secret values are intentionally excluded. +- Registration behavior also depends on fields stored in the Directus settings table. +- Role, policy, and permission exports are stored separately. +- Flow option values are excluded because they may contain credentials, headers, addresses, or other sensitive configuration. diff --git a/directus/reference/docker-compose.sanitized.yml b/directus/reference/docker-compose.sanitized.yml new file mode 100644 index 0000000..dde2e97 --- /dev/null +++ b/directus/reference/docker-compose.sanitized.yml @@ -0,0 +1,28 @@ +services: + directus: + image: directus/directus:latest + container_name: directus + ports: + - "127.0.0.1:8056:8055" + env_file: + - .env + volumes: + - ./uploads:/directus/uploads + - ./extensions:/directus/extensions + networks: + - default + - backend_net + restart: unless-stopped + +networks: + default: + name: directus_net + driver: bridge + enable_ipv6: true + ipam: + config: + - subnet: 10.40.0.0/16 + - subnet: fd00:40::/64 + backend_net: + external: true + diff --git a/directus/reference/environment-variable-names.txt b/directus/reference/environment-variable-names.txt new file mode 100644 index 0000000..c0e194d --- /dev/null +++ b/directus/reference/environment-variable-names.txt @@ -0,0 +1,24 @@ +ADMIN_EMAIL +ADMIN_PASSWORD +DB_CLIENT +DB_DATABASE +DB_FILENAME +DB_HOST +DB_PASSWORD +DB_PORT +DB_USER +EMAIL_FROM +EMAIL_SMTP_HOST +EMAIL_SMTP_PASSWORD +EMAIL_SMTP_PORT +EMAIL_SMTP_SECURE +EMAIL_SMTP_USER +EMAIL_TRANSPORT +KEY +NODE_ENV +NODE_VERSION +NPM_CONFIG_UPDATE_NOTIFIER +PATH +PUBLIC_URL +SECRET +YARN_VERSION diff --git a/directus/reference/exports/access.json b/directus/reference/exports/access.json new file mode 100644 index 0000000..b9e1940 --- /dev/null +++ b/directus/reference/exports/access.json @@ -0,0 +1,44 @@ +[ + { + "id": "45b5000f-f495-4940-935a-d4bf2e409844", + "role": "a6d3c757-a96b-4f0c-b96b-a6109d60a7ba", + "policy": "93fc7a9e-16de-45d4-89b1-9fa129e81442", + "sort": null, + "user": null + }, + { + "id": "2ddbc9da-e0f5-4dff-a952-c3d7586ab706", + "role": "296a28bc-60ab-4251-8bef-27f6dfb67948", + "policy": "80b3902c-06e6-480c-9d29-d2a46552f60e", + "sort": 1, + "user": null + }, + { + "id": "767277dd-3b08-4d72-9bde-437af91e1d88", + "role": "a2a695be-88f9-4948-b2a4-e0e22b01b32e", + "policy": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded", + "sort": 1, + "user": null + }, + { + "id": "abfa5434-1ace-4e1f-9a93-7d84aabb0a95", + "role": "00e518e3-75b3-4c6f-b57d-51a4f1af4781", + "policy": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001", + "sort": 1, + "user": null + }, + { + "id": "c1480ea2-ca4d-46c9-8034-09b76d0a41cf", + "role": null, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "sort": 1, + "user": null + }, + { + "id": "cf8e40bd-3fbe-4df5-a845-01802a3e7e1c", + "role": "296a28bc-60ab-4251-8bef-27f6dfb67948", + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "sort": 2, + "user": null + } +] diff --git a/directus/reference/exports/directus-user-fields.json b/directus/reference/exports/directus-user-fields.json new file mode 100644 index 0000000..e257da7 --- /dev/null +++ b/directus/reference/exports/directus-user-fields.json @@ -0,0 +1,19 @@ +[ + { + "field": "username", + "special": null, + "interface": "input", + "options": null, + "display": "raw", + "display_options": null, + "readonly": 0, + "hidden": 0, + "required": 1, + "sort": 1, + "width": "full", + "note": null, + "conditions": null, + "validation": null, + "validation_message": null + } +] diff --git a/directus/reference/exports/flows.json b/directus/reference/exports/flows.json new file mode 100644 index 0000000..02deaef --- /dev/null +++ b/directus/reference/exports/flows.json @@ -0,0 +1,35 @@ +[ + { + "id": "fdc2d675-e365-4431-9cd0-27e571c89e04", + "name": "claim_auto_assign", + "icon": "bolt", + "color": null, + "description": "Assigns a claimant to owner and rejects other pending claims.", + "status": "active", + "trigger": "event", + "accountability": "all", + "options_keys": [ + "type", + "scope", + "collections" + ], + "root_operation": "e19723a6-e75c-4bc8-92aa-be06bb0528af" + }, + { + "id": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092", + "name": "unique_key_claims", + "icon": "bolt", + "color": null, + "description": "A flow for setting a unique key for each claim to prevent duplicates.", + "status": "active", + "trigger": "event", + "accountability": "all", + "options_keys": [ + "type", + "scope", + "collections", + "return" + ], + "root_operation": "2245466f-8178-4e5c-b058-819eb421de47" + } +] diff --git a/directus/reference/exports/operations.json b/directus/reference/exports/operations.json new file mode 100644 index 0000000..f6ebdcd --- /dev/null +++ b/directus/reference/exports/operations.json @@ -0,0 +1,94 @@ +[ + { + "id": "03c6582f-800d-4e94-9fea-f77a9ea5e3aa", + "name": "Condition", + "key": "condition_s458i", + "type": "condition", + "position_x": 55, + "position_y": 1, + "resolve": "d4266993-ceae-452a-9b7d-664c11bf1013", + "reject": null, + "flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092", + "options_keys": [ + "filter" + ] + }, + { + "id": "cb7101fc-e2c5-4cfc-b1d6-486ac70d286f", + "name": "Read Data", + "key": "item_read_e4myo", + "type": "item-read", + "position_x": 37, + "position_y": 1, + "resolve": "03c6582f-800d-4e94-9fea-f77a9ea5e3aa", + "reject": null, + "flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092", + "options_keys": [ + "permissions", + "emitEvents", + "collection", + "query" + ] + }, + { + "id": "2245466f-8178-4e5c-b058-819eb421de47", + "name": "Run Script", + "key": "exec_5dli5", + "type": "exec", + "position_x": 19, + "position_y": 1, + "resolve": "cb7101fc-e2c5-4cfc-b1d6-486ac70d286f", + "reject": null, + "flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092", + "options_keys": [ + "code" + ] + }, + { + "id": "d4266993-ceae-452a-9b7d-664c11bf1013", + "name": "Throw Error", + "key": "throw_error_22tpm", + "type": "throw-error", + "position_x": 73, + "position_y": 1, + "resolve": null, + "reject": null, + "flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092", + "options_keys": [ + "status", + "message", + "code" + ] + }, + { + "id": "e19723a6-e75c-4bc8-92aa-be06bb0528af", + "name": "Condition", + "key": "condition_dl7cj", + "type": "condition", + "position_x": 19, + "position_y": 1, + "resolve": "ebd0e644-1be0-4200-89c4-844b06c7d788", + "reject": null, + "flow": "fdc2d675-e365-4431-9cd0-27e571c89e04", + "options_keys": [ + "filter" + ] + }, + { + "id": "ebd0e644-1be0-4200-89c4-844b06c7d788", + "name": "Read Data", + "key": "item_read_oyges", + "type": "item-read", + "position_x": 37, + "position_y": 1, + "resolve": null, + "reject": null, + "flow": "fdc2d675-e365-4431-9cd0-27e571c89e04", + "options_keys": [ + "permissions", + "emitEvents", + "collection", + "key" + ] + } +] diff --git a/directus/reference/exports/permissions.json b/directus/reference/exports/permissions.json new file mode 100644 index 0000000..770a6f4 --- /dev/null +++ b/directus/reference/exports/permissions.json @@ -0,0 +1,1176 @@ +[ + { + "id": 204, + "policy": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded", + "collection": "user_memberships", + "action": "create", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 205, + "policy": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded", + "collection": "user_memberships", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 206, + "policy": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded", + "collection": "user_memberships", + "action": "update", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 136, + "policy": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001", + "collection": "directus_roles", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "id,name,icon,description,parent,children" + }, + { + "id": 135, + "policy": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001", + "collection": "directus_users", + "action": "create", + "permissions": null, + "validation": null, + "presets": null, + "fields": "email,username,password,role,status,auth_data,first_name,last_name,avatar,id" + }, + { + "id": 134, + "policy": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001", + "collection": "directus_users", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "username,email,id,first_name,last_name,password,avatar,location,title,description,tags,preferences_divider,language,text_direction,tfa_secret,auth_data,external_identifier,provider,last_access,last_page,token,policies,role,status,admin_divider,theme_dark_overrides,theme_dark,theme_light_overrides,theme_light,appearance,theming_divider,email_notifications" + }, + { + "id": 199, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "bg_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 198, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "bg_entries", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 197, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "bg_sub_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 181, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "directus_fields", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "id,collection,field,special,interface,options,readonly,hidden,required,sort,width,group,note" + }, + { + "id": 172, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "directus_files", + "action": "create", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 173, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "directus_files", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 133, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "directus_users", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "username,first_name,avatar,location,id" + }, + { + "id": 196, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "hazard_danger", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 195, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "hazard_severity", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 194, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "hazard_source", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 193, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 150, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_focus_lens", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 149, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_focus_lens_config", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 148, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_focus_lens_diameter", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 147, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_scan_lens", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 146, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_scan_lens_apt", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 145, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_scan_lens_config", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 144, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_scan_lens_exp", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 143, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_software", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 142, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "laser_source", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 192, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 191, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 190, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_coating", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 189, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_coating_hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 188, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_color", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 187, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 186, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_opacity", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 185, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "material_status", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 160, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER", + "uploader": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 171, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 161, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 170, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 200, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects_files", + "action": "create", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 184, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "projects_files", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 155, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gal", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER", + "uploader": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 169, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gal", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 156, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gal", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 168, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gal", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 154, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gan", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER", + "uploader": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 167, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gan", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 157, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gan", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 166, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_co2gan", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 153, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_fiber", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER", + "uploader": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 165, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_fiber", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 158, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_fiber", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 164, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_fiber", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 152, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_uv", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER", + "uploader": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 163, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_uv", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": null + }, + { + "id": 159, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_uv", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 162, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "settings_uv", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 176, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_claims", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "claimant": "$CURRENT_USER", + "status": "pending" + }, + "fields": "id,target_collection,target_id,sort,user_created,date_created,user_updated,date_updated,note" + }, + { + "id": 179, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_claims", + "action": "delete", + "permissions": { + "_and": [ + { + "claimant": { + "_eq": "$CURRENT_USER" + } + }, + { + "status": { + "_eq": "pending" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": null + }, + { + "id": 177, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_claims", + "action": "read", + "permissions": { + "_and": [ + { + "claimant": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "id,target_collection,target_id,status,note,reviewed_by,reviewed_at,date_created" + }, + { + "id": 178, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_claims", + "action": "update", + "permissions": { + "_and": [ + { + "claimant": { + "_eq": "$CURRENT_USER" + } + }, + { + "status": { + "_eq": "pending" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "note" + }, + { + "id": 137, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rigs", + "action": "create", + "permissions": null, + "validation": null, + "presets": { + "owner": "$CURRENT_USER" + }, + "fields": "*" + }, + { + "id": 140, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rigs", + "action": "delete", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 138, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rigs", + "action": "read", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 139, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rigs", + "action": "update", + "permissions": { + "_and": [ + { + "owner": { + "_eq": "%CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 201, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rig_type", + "action": "create", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 141, + "policy": "536f020c-4f05-497d-8a6c-54e13f62c694", + "collection": "user_rig_type", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 180, + "policy": "80b3902c-06e6-480c-9d29-d2a46552f60e", + "collection": "directus_users", + "action": "read", + "permissions": { + "_and": [ + { + "id": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "username,last_name,first_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications,id" + }, + { + "id": 203, + "policy": "80b3902c-06e6-480c-9d29-d2a46552f60e", + "collection": "directus_users", + "action": "update", + "permissions": { + "_and": [ + { + "id": { + "_eq": "$CURRENT_USER" + } + } + ] + }, + "validation": null, + "presets": null, + "fields": "first_name,last_name,email,password,avatar,location" + }, + { + "id": 89, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "bg_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 92, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "bg_entries", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 95, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "bg_sub_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 182, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "directus_fields", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "field,collection,id" + }, + { + "id": 50, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "directus_files", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 47, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "hazard_danger", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 44, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "hazard_severity", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 41, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "hazard_source", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 38, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 65, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_focus_lens", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 68, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_focus_lens_config", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 71, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_focus_lens_diameter", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 35, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_scan_lens", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 74, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_scan_lens_apt", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 77, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_scan_lens_config", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 80, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_scan_lens_exp", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 32, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_software", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 11, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "laser_source", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 8, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 30, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_cat", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 5, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_coating", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 26, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_coating_hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 23, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_color", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 20, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_hazard_tags", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 17, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_opacity", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 13, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "material_status", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 53, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "projects", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 56, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "projects_files", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 83, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "settings_co2gal", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 86, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "settings_co2gan", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 2, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "settings_fiber", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + }, + { + "id": 59, + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "collection": "settings_uv", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": "*" + } +] diff --git a/directus/reference/exports/policies.json b/directus/reference/exports/policies.json new file mode 100644 index 0000000..cb43960 --- /dev/null +++ b/directus/reference/exports/policies.json @@ -0,0 +1,62 @@ +[ + { + "id": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "name": "$t:public_label", + "icon": "public", + "description": "$t:public_description", + "admin_access": 0, + "app_access": 0, + "enforce_tfa": 0, + "ip_access": null + }, + { + "id": "93fc7a9e-16de-45d4-89b1-9fa129e81442", + "name": "Administrator", + "icon": "verified", + "description": "$t:admin_description", + "admin_access": 1, + "app_access": 1, + "enforce_tfa": 0, + "ip_access": null + }, + { + "id": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001", + "name": "Registration", + "icon": "robot", + "description": "Registration Bot", + "admin_access": 0, + "app_access": 0, + "enforce_tfa": 0, + "ip_access": null + }, + { + "id": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded", + "name": "Supporter", + "icon": "robot_2", + "description": "Supporter Bot", + "admin_access": 0, + "app_access": 0, + "enforce_tfa": 0, + "ip_access": null + }, + { + "id": "536f020c-4f05-497d-8a6c-54e13f62c694", + "name": "Users", + "icon": "badge", + "description": "Registered Users", + "admin_access": 0, + "app_access": 0, + "enforce_tfa": 0, + "ip_access": null + }, + { + "id": "80b3902c-06e6-480c-9d29-d2a46552f60e", + "name": "Users Self Access", + "icon": "badge", + "description": "Registered Users Self Access", + "admin_access": 0, + "app_access": 0, + "enforce_tfa": 0, + "ip_access": null + } +] diff --git a/directus/reference/exports/roles.json b/directus/reference/exports/roles.json new file mode 100644 index 0000000..8847f19 --- /dev/null +++ b/directus/reference/exports/roles.json @@ -0,0 +1,30 @@ +[ + { + "id": "a6d3c757-a96b-4f0c-b96b-a6109d60a7ba", + "name": "Administrator", + "icon": "verified", + "description": "$t:admin_description", + "parent": null + }, + { + "id": "00e518e3-75b3-4c6f-b57d-51a4f1af4781", + "name": "Registration Bot", + "icon": "robot", + "description": "Registers accounts.", + "parent": null + }, + { + "id": "a2a695be-88f9-4948-b2a4-e0e22b01b32e", + "name": "Supporter Bot", + "icon": "supervised_user_circle", + "description": "Registers supporters.", + "parent": null + }, + { + "id": "296a28bc-60ab-4251-8bef-27f6dfb67948", + "name": "Users", + "icon": "accessibility_new", + "description": "Registered users.", + "parent": null + } +] diff --git a/directus/reference/exports/settings.json b/directus/reference/exports/settings.json new file mode 100644 index 0000000..dc9f23c --- /dev/null +++ b/directus/reference/exports/settings.json @@ -0,0 +1,11 @@ +[ + { + "project_name": "Directus", + "project_url": null, + "default_language": "en-US", + "public_registration": 0, + "public_registration_verify_email": 0, + "public_registration_role": null, + "public_registration_email_filter": null + } +] diff --git a/directus/reference/nginx-summary.txt b/directus/reference/nginx-summary.txt new file mode 100644 index 0000000..ea57905 --- /dev/null +++ b/directus/reference/nginx-summary.txt @@ -0,0 +1,33 @@ +3: listen 80; +4: server_name forms.lasereverything.net; +5: client_max_body_size 100M; +8: location /.well-known/acme-challenge/ { +13: location / { +20: listen 443 ssl; +22: server_name forms.lasereverything.net; +23: client_max_body_size 100M; +28: ssl_protocols TLSv1.2 TLSv1.3; +31: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +33: location / { +34: proxy_pass http://127.0.0.1:8056; +37: proxy_set_header Host $host; +38: proxy_set_header X-Real-IP $remote_addr; +39: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +40: proxy_set_header X-Forwarded-Proto $scheme; +43: proxy_set_header Upgrade $http_upgrade; +44: proxy_set_header Connection "upgrade"; +46: proxy_read_timeout 300; +48: proxy_send_timeout 300; +57: add_header Access-Control-Allow-Origin "$http_origin" always; +58: add_header Access-Control-Allow-Credentials "true" always; +59: add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always; +60: add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always; +61: add_header Vary "Origin" always; +66: add_header Access-Control-Max-Age 1728000 always; +67: add_header Content-Type "text/plain; charset=utf-8" always; +68: add_header Content-Length 0 always; +71: add_header Access-Control-Allow-Origin "$http_origin" always; +72: add_header Access-Control-Allow-Credentials "true" always; +73: add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always; +74: add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always; +75: add_header Vary "Origin" always; diff --git a/directus/reference/schema-comparison.txt b/directus/reference/schema-comparison.txt new file mode 100644 index 0000000..a44985c --- /dev/null +++ b/directus/reference/schema-comparison.txt @@ -0,0 +1,8 @@ +Live schema SHA-256: +709c5f5fa6c239406085e8c7e5959d2d096d36ad1d8676727a0ca473536498e6 /root/directus-codex-context-20260710-102253/schema.yaml + +Existing repository schema SHA-256: +8e21cd555b297e729a39f387eadbf4d32621c986516c9d3cc896b01d02efdf8b /srv/codex-work/lasereverything.net.db/app/schema.yaml + +Comparison: +The live and repository schema files differ. diff --git a/directus/reference/system-table-columns/directus_access-columns.tsv b/directus/reference/system-table-columns/directus_access-columns.tsv new file mode 100644 index 0000000..c159bfc --- /dev/null +++ b/directus/reference/system-table-columns/directus_access-columns.tsv @@ -0,0 +1,5 @@ +id char(36) NO PRI NULL +role char(36) YES MUL NULL +user char(36) YES MUL NULL +policy char(36) NO MUL NULL +sort int(11) YES NULL diff --git a/directus/reference/system-table-columns/directus_fields-columns.tsv b/directus/reference/system-table-columns/directus_fields-columns.tsv new file mode 100644 index 0000000..fe3341d --- /dev/null +++ b/directus/reference/system-table-columns/directus_fields-columns.tsv @@ -0,0 +1,20 @@ +id int(10) unsigned NO PRI NULL auto_increment +collection varchar(64) NO MUL NULL +field varchar(64) NO NULL +special varchar(64) YES NULL +interface varchar(64) YES NULL +options longtext YES NULL +display varchar(64) YES NULL +display_options longtext YES NULL +readonly tinyint(1) NO 0 +hidden tinyint(1) NO 0 +sort int(10) unsigned YES NULL +width varchar(30) YES full +translations longtext YES NULL +note text YES NULL +conditions longtext YES NULL +required tinyint(1) YES 0 +group varchar(64) YES NULL +validation longtext YES NULL +validation_message text YES NULL +searchable tinyint(1) NO 1 diff --git a/directus/reference/system-table-columns/directus_flows-columns.tsv b/directus/reference/system-table-columns/directus_flows-columns.tsv new file mode 100644 index 0000000..297b778 --- /dev/null +++ b/directus/reference/system-table-columns/directus_flows-columns.tsv @@ -0,0 +1,12 @@ +id char(36) NO PRI NULL +name varchar(255) NO NULL +icon varchar(64) YES NULL +color varchar(255) YES NULL +description text YES NULL +status varchar(255) NO active +trigger varchar(255) YES NULL +accountability varchar(255) YES all +options longtext YES NULL +operation char(36) YES UNI NULL +date_created timestamp YES current_timestamp() +user_created char(36) YES MUL NULL diff --git a/directus/reference/system-table-columns/directus_operations-columns.tsv b/directus/reference/system-table-columns/directus_operations-columns.tsv new file mode 100644 index 0000000..b60950b --- /dev/null +++ b/directus/reference/system-table-columns/directus_operations-columns.tsv @@ -0,0 +1,12 @@ +id char(36) NO PRI NULL +name varchar(255) YES NULL +key varchar(255) NO NULL +type varchar(255) NO NULL +position_x int(11) NO NULL +position_y int(11) NO NULL +options longtext YES NULL +resolve char(36) YES UNI NULL +reject char(36) YES UNI NULL +flow char(36) NO MUL NULL +date_created timestamp YES current_timestamp() +user_created char(36) YES MUL NULL diff --git a/directus/reference/system-table-columns/directus_permissions-columns.tsv b/directus/reference/system-table-columns/directus_permissions-columns.tsv new file mode 100644 index 0000000..0c32c46 --- /dev/null +++ b/directus/reference/system-table-columns/directus_permissions-columns.tsv @@ -0,0 +1,8 @@ +id int(10) unsigned NO PRI NULL auto_increment +collection varchar(64) NO MUL NULL +action varchar(10) NO NULL +permissions longtext YES NULL +validation longtext YES NULL +presets longtext YES NULL +fields text YES NULL +policy char(36) NO MUL NULL diff --git a/directus/reference/system-table-columns/directus_policies-columns.tsv b/directus/reference/system-table-columns/directus_policies-columns.tsv new file mode 100644 index 0000000..4353e7f --- /dev/null +++ b/directus/reference/system-table-columns/directus_policies-columns.tsv @@ -0,0 +1,8 @@ +id char(36) NO PRI NULL +name varchar(100) NO NULL +icon varchar(64) NO badge +description text YES NULL +ip_access text YES NULL +enforce_tfa tinyint(1) NO 0 +admin_access tinyint(1) NO 0 +app_access tinyint(1) NO 0 diff --git a/directus/reference/system-table-columns/directus_roles-columns.tsv b/directus/reference/system-table-columns/directus_roles-columns.tsv new file mode 100644 index 0000000..992e2ea --- /dev/null +++ b/directus/reference/system-table-columns/directus_roles-columns.tsv @@ -0,0 +1,5 @@ +id char(36) NO PRI NULL +name varchar(100) NO NULL +icon varchar(64) NO supervised_user_circle +description text YES NULL +parent char(36) YES MUL NULL diff --git a/directus/reference/system-table-columns/directus_settings-columns.tsv b/directus/reference/system-table-columns/directus_settings-columns.tsv new file mode 100644 index 0000000..d2af55b --- /dev/null +++ b/directus/reference/system-table-columns/directus_settings-columns.tsv @@ -0,0 +1,66 @@ +id int(10) unsigned NO PRI NULL auto_increment +project_name varchar(100) NO Directus +project_url varchar(255) YES NULL +project_color varchar(255) NO #6644FF +project_logo char(36) YES MUL NULL +public_foreground char(36) YES MUL NULL +public_background char(36) YES MUL NULL +public_note text YES NULL +auth_login_attempts int(10) unsigned YES 25 +auth_password_policy varchar(100) YES NULL +storage_asset_transform varchar(7) YES all +storage_asset_presets longtext YES NULL +custom_css text YES NULL +storage_default_folder char(36) YES MUL NULL +basemaps longtext YES NULL +mapbox_key varchar(255) YES NULL +module_bar longtext YES NULL +project_descriptor varchar(100) YES NULL +default_language varchar(255) NO en-US +custom_aspect_ratios longtext YES NULL +public_favicon char(36) YES MUL NULL +default_appearance varchar(255) NO auto +default_theme_light varchar(255) YES NULL +theme_light_overrides longtext YES NULL +default_theme_dark varchar(255) YES NULL +theme_dark_overrides longtext YES NULL +report_error_url varchar(255) YES NULL +report_bug_url varchar(255) YES NULL +report_feature_url varchar(255) YES NULL +public_registration tinyint(1) NO 0 +public_registration_verify_email tinyint(1) NO 1 +public_registration_role char(36) YES MUL NULL +public_registration_email_filter longtext YES NULL +visual_editor_urls longtext YES NULL +project_id char(36) YES NULL +mcp_enabled tinyint(1) NO 0 +mcp_allow_deletes tinyint(1) NO 0 +mcp_prompts_collection varchar(255) YES NULL +mcp_system_prompt_enabled tinyint(1) NO 1 +mcp_system_prompt text YES NULL +project_owner varchar(255) YES NULL +project_usage varchar(255) YES NULL +org_name varchar(255) YES NULL +product_updates tinyint(1) YES NULL +project_status varchar(255) YES NULL +ai_openai_api_key text YES NULL +ai_anthropic_api_key text YES NULL +ai_system_prompt text YES NULL +ai_google_api_key text YES NULL +ai_openai_compatible_api_key text YES NULL +ai_openai_compatible_base_url text YES NULL +ai_openai_compatible_name text YES NULL +ai_openai_compatible_models longtext YES NULL +ai_openai_compatible_headers longtext YES NULL +ai_openai_allowed_models longtext YES NULL +ai_anthropic_allowed_models longtext YES NULL +ai_google_allowed_models longtext YES NULL +collaborative_editing_enabled tinyint(1) NO 0 +ai_translation_default_model text YES NULL +ai_translation_glossary longtext YES NULL +ai_translation_style_guide text YES NULL +license_key varchar(255) YES NULL +license_token text YES NULL +mcp_oauth_enabled tinyint(1) NO 0 +mcp_oauth_dcr_enabled tinyint(1) NO 0 +mcp_oauth_cimd_enabled tinyint(1) NO 0 diff --git a/directus/schema-differences.md b/directus/schema-differences.md new file mode 100644 index 0000000..a2da87c --- /dev/null +++ b/directus/schema-differences.md @@ -0,0 +1,55 @@ +# Production versus older schema snapshot + +Compared files: + +- Current production evidence: `directus/schema.yaml`, Directus 12.1.1, MySQL. +- Older repository snapshot: `app/schema.yaml`, Directus 11.12.0, MySQL. + +The supplied hashes differ. A structural YAML comparison found 36 versus 34 collection entries, 394 versus 379 field entries, and 138 versus 136 relation entries. Neither snapshot was modified. + +## Meaningful additions + +### `directus_users` collection metadata + +The current snapshot includes `directus_users` as a collection with display template `{{ username }} ({{ email }})`. The older snapshot contains the custom `directus_users.username` field and relations to the system collection but omits the collection entry itself. + +The username field remains a unique nullable `varchar(255)` at the database layer while Directus metadata marks it required, visible, editable, and searchable. The older snapshot has the same database and required-field characteristics; current metadata additionally contains the Directus 12 `searchable` property. + +### `user_memberships` + +Production adds the `user_memberships` collection with 15 fields: + +- Integer auto-increment primary key: `id`. +- Provider/account data: `provider`, `external_user_id`, `email`, `username`, `status`, `tier`. +- Timing: `started_at`, `renews_at`, `last_event_at`. +- Claim/support data: `claim_token`, `claim_expires_at`, `raw`. +- User relations: `app_user`, `claim_user_id`. + +`app_user` and `claim_user_id` are nullable indexed M2O foreign keys to `directus_users.id`; both use `SET NULL` on delete and `RESTRICT` on update. The schema does not mark `claim_token` unique. Provider choices are Ko-Fi, Patreon, and LMA/Mighty; status choices are active, canceled, refunded, and one-time. These are metadata choices, not database constraints. + +### Settings raster metadata + +The `settings_co2gal.raster_settings` repeater gained three nested metadata fields: `angle` (integer), `auto` (boolean), and `increment` (integer). These are changes inside the JSON/repeater field definition, not new top-level database columns. + +### Display metadata + +- `settings_co2gal.owner` changed from no display renderer to related-values with template `{{username}}`. +- `bg_entries.scores` changed its display formatting template from `{{ cat }}` to `{{ body }}`. + +## Version-generated metadata differences + +Most raw differences are serialization changes rather than business-schema changes: + +- All 34 pre-existing collection entries gained `meta.autosave_revision_interval: null` and `meta.status: active`. +- All 379 pre-existing field entries gained `meta.searchable: true`. + +These additions are consistent across the snapshots and correspond to newer snapshot metadata emitted by Directus 12. They do not by themselves show database-column changes. + +## Unchanged structural areas + +- No older collection, field, or relation key was removed. +- All 136 relations present in the older snapshot compare equal structurally. +- The two added relations are the new `user_memberships` links described above. +- Database vendor remains MySQL and snapshot format version remains 1. + +This comparison is structural and does not compare data, permissions, policies, flows, settings-table values, indexes not represented by the snapshot, extension behavior, or migration history. diff --git a/directus/schema.yaml b/directus/schema.yaml new file mode 100644 index 0000000..2d40d34 --- /dev/null +++ b/directus/schema.yaml @@ -0,0 +1,22571 @@ +version: 1 +directus: 12.1.1 +vendor: mysql +collections: + - collection: bg_cat + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: bg_cat + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: bg_cat + - collection: bg_entries + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: bg_entries + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: bg_entries + - collection: bg_sub_cat + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: bg_sub_cat + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: bg_sub_cat + - collection: directus_users + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: directus_users + color: null + display_template: '{{ username }} ({{ email }})' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: directus_users + - collection: hazard_danger + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: hazard_danger + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 1 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: hazard_danger + - collection: hazard_severity + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: hazard_severity + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 2 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: hazard_severity + - collection: hazard_source + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: hazard_source + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 3 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: hazard_source + - collection: hazard_tags + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: hazard_tags + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 4 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: hazard_tags + - collection: laser_focus_lens + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_focus_lens + color: null + display_template: '{{name}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 5 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_focus_lens + - collection: laser_focus_lens_config + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_focus_lens_config + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 6 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_focus_lens_config + - collection: laser_focus_lens_diameter + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_focus_lens_diameter + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 7 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_focus_lens_diameter + - collection: laser_scan_lens + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_scan_lens + color: null + display_template: '{{field_size}} {{focal_length}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 8 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_scan_lens + - collection: laser_scan_lens_apt + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_scan_lens_apt + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 10 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_scan_lens_apt + - collection: laser_scan_lens_config + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_scan_lens_config + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 9 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_scan_lens_config + - collection: laser_scan_lens_exp + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_scan_lens_exp + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 11 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_scan_lens_exp + - collection: laser_software + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_software + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 12 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_software + - collection: laser_source + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: laser_source + color: null + display_template: '{{make}} {{model}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 13 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: laser_source + - collection: material + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 14 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material + - collection: material_cat + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_cat + color: null + display_template: '{{name}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 15 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_cat + - collection: material_coating + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_coating + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 16 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_coating + - collection: material_coating_hazard_tags + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_coating_hazard_tags + color: null + display_template: null + group: null + hidden: false + icon: import_export + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 17 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_coating_hazard_tags + - collection: material_color + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_color + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 18 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_color + - collection: material_hazard_tags + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_hazard_tags + color: null + display_template: null + group: null + hidden: false + icon: import_export + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 19 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_hazard_tags + - collection: material_opacity + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_opacity + color: null + display_template: '{{opacity}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 20 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_opacity + - collection: material_status + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: material_status + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 21 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: material_status + - collection: projects + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: projects + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 22 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: projects + - collection: projects_files + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: projects_files + color: null + display_template: null + group: null + hidden: false + icon: import_export + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 23 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: projects_files + - collection: settings_co2gal + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: settings_co2gal + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 24 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: settings_co2gal + - collection: settings_co2gan + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: settings_co2gan + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 25 + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: settings_co2gan + - collection: settings_fiber + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: settings_fiber + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 26 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: settings_fiber + - collection: settings_uv + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: settings_uv + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: 27 + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: settings_uv + - collection: user_claims + meta: + accountability: all + archive_app_filter: true + archive_field: status + archive_value: archived + autosave_revision_interval: null + collapse: open + collection: user_claims + color: null + display_template: '{{target_collection}} #{{target_id}} — {{status}} — {{claimant.email}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: draft + versioning: false + schema: + name: user_claims + - collection: user_memberships + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: user_memberships + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: null + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: user_memberships + - collection: user_preferences + meta: + accountability: all + archive_app_filter: true + archive_field: status + archive_value: archived + autosave_revision_interval: null + collapse: open + collection: user_preferences + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: draft + versioning: false + schema: + name: user_preferences + - collection: user_rig_type + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: user_rig_type + color: null + display_template: '{{name}}' + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: user_rig_type + - collection: user_rigs + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + autosave_revision_interval: null + collapse: open + collection: user_rigs + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: sort + status: active + translations: null + unarchive_value: null + versioning: false + schema: + name: user_rigs +fields: + - collection: bg_cat + field: id + type: integer + meta: + collection: bg_cat + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: bg_cat + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: bg_cat + field: sort + type: integer + meta: + collection: bg_cat + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: bg_cat + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_cat + field: user_created + type: string + meta: + collection: bg_cat + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 4 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: bg_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_cat + field: date_created + type: timestamp + meta: + collection: bg_cat + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: bg_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_cat + field: user_updated + type: string + meta: + collection: bg_cat + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 6 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: bg_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_cat + field: date_updated + type: timestamp + meta: + collection: bg_cat + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: bg_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_cat + field: name + type: string + meta: + collection: bg_cat + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: bg_cat + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: id + type: integer + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: bg_entries + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: sort + type: integer + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 17 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: bg_entries + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: user_created + type: string + meta: + collection: bg_entries + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 18 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: bg_entries + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_entries + field: date_created + type: timestamp + meta: + collection: bg_entries + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 19 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: bg_entries + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: user_updated + type: string + meta: + collection: bg_entries + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 20 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: bg_entries + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_entries + field: date_updated + type: timestamp + meta: + collection: bg_entries + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 21 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: bg_entries + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: product_make + type: string + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: product_make + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: product_make + table: bg_entries + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: product_model + type: string + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: product_model + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: product_model + table: bg_entries + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: product_price + type: string + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: product_price + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: product_price + table: bg_entries + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: video_review_url + type: string + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: video_review_url + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: video_review_url + table: bg_entries + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: links + type: json + meta: + collection: bg_entries + conditions: null + display: raw + display_options: null + field: links + group: null + hidden: false + interface: list + note: null + options: + addLabel: Add a Link + fields: + - field: text + meta: + display: formatted-value + display_options: + conditionalFormatting: null + format: true + field: text + interface: input + options: + iconLeft: text_fields + placeholder: display text + type: string + width: full + name: text + type: string + - field: url + meta: + display: formatted-value + display_options: + format: true + field: url + interface: input + options: + iconLeft: link + placeholder: https://www.amazon.com/2308f0mi + type: string + name: url + type: string + - field: target + meta: + display: formatted-value + display_options: + format: true + field: target + interface: input + options: + iconLeft: shopping_cart_checkout + placeholder: Amazon, eBay, Etsy, etc... + type: string + name: target + type: string + template: links + readonly: false + required: false + searchable: true + sort: 10 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: links + table: bg_entries + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: author + type: string + meta: + collection: bg_entries + conditions: null + display: formatted-value + display_options: + format: true + field: author + group: null + hidden: false + interface: input + note: null + options: + iconLeft: edit_square + placeholder: Your Mother + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: author + table: bg_entries + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: review_overview_text + type: text + meta: + collection: bg_entries + conditions: null + display: formatted-value + display_options: + format: true + field: review_overview_text + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: d397e79a-d422-43a3-b16a-5edf0cd67aad + placeholder: >- + Give an overview, if there's no full review only fill out this + section. + readonly: false + required: false + searchable: true + sort: 12 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: review_overview_text + table: bg_entries + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: review_intro_text + type: text + meta: + collection: bg_entries + conditions: null + display: formatted-value + display_options: + format: true + field: review_intro_text + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 767d0434-e309-400c-8c48-04da55b53a22 + placeholder: Introduce your review and give a preview of what's to come. + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: review_intro_text + table: bg_entries + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: scores + type: json + meta: + collection: bg_entries + conditions: null + display: formatted-json-value + display_options: + format: '{{ body }}' + field: scores + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Score + fields: + - field: value + meta: + display: formatted-value + display_options: + format: true + field: value + interface: slider + options: + maxValue: 10 + minValue: 1 + stepInterval: 1 + type: float + name: value + type: float + - field: body + meta: + display: formatted-value + display_options: + format: true + field: body + interface: input-rich-text-md + options: + folder: 1358acff-a633-4bd6-a620-80e2b95afe77 + type: text + name: body + type: text + - field: cat + meta: + display: formatted-value + display_options: + format: true + field: cat + interface: select-dropdown + options: + choices: + - text: CONTENTS & ASSEMBLY + value: cont_assembly + - text: HARDWARE & SPECS + value: hard_specs + - text: LASER MODULE + value: las_mod + - text: PERFORMANCE + value: perf + - text: SOFTWARE & USABILITY + value: soft_use + - text: COST & VALUE + value: cost_val + type: string + name: cat + type: string + template: scores + readonly: false + required: false + searchable: true + sort: 14 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: scores + table: bg_entries + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: rec_text + type: text + meta: + collection: bg_entries + conditions: null + display: formatted-value + display_options: + format: true + field: rec_text + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 56733eb9-2517-471a-8e0e-f771051991b0 + placeholder: Do you recommend this? Why or why not? + readonly: false + required: false + searchable: true + sort: 15 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: rec_text + table: bg_entries + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: updates + type: text + meta: + collection: bg_entries + conditions: null + display: formatted-value + display_options: + format: true + field: updates + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: d35dc3f2-e29e-4afb-bf4b-bb16ae887bb6 + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: updates + table: bg_entries + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_entries + field: index + type: uuid + meta: + collection: bg_entries + conditions: null + display: null + display_options: null + field: index + group: null + hidden: false + interface: file-image + note: null + options: + folder: a7ccad88-3b8c-42a1-8d8d-ac5cfa92f25e + readonly: false + required: false + searchable: true + sort: 2 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: index + table: bg_entries + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: bg_entries + field: header + type: uuid + meta: + collection: bg_entries + conditions: null + display: image + display_options: null + field: header + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: 954f70c8-fa62-4b5a-b5e6-975b538f95d8 + readonly: false + required: false + searchable: true + sort: 3 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: header + table: bg_entries + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: bg_entries + field: bg_entry_sub_cat + type: integer + meta: + collection: bg_entries + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: bg_entry_sub_cat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 5 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: bg_entry_sub_cat + table: bg_entries + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: bg_sub_cat + foreign_key_column: id + - collection: bg_entries + field: bg_entry_cat + type: integer + meta: + collection: bg_entries + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: bg_entry_cat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: bg_entry_cat + table: bg_entries + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: bg_cat + foreign_key_column: id + - collection: bg_sub_cat + field: id + type: integer + meta: + collection: bg_sub_cat + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: bg_sub_cat + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: bg_sub_cat + field: sort + type: integer + meta: + collection: bg_sub_cat + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: bg_sub_cat + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_sub_cat + field: user_created + type: string + meta: + collection: bg_sub_cat + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: bg_sub_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_sub_cat + field: date_created + type: timestamp + meta: + collection: bg_sub_cat + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: bg_sub_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_sub_cat + field: user_updated + type: string + meta: + collection: bg_sub_cat + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 7 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: bg_sub_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: bg_sub_cat + field: date_updated + type: timestamp + meta: + collection: bg_sub_cat + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 8 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: bg_sub_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_sub_cat + field: name + type: string + meta: + collection: bg_sub_cat + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: bg_sub_cat + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: bg_sub_cat + field: bg_entry_cat + type: integer + meta: + collection: bg_sub_cat + conditions: null + display: null + display_options: null + field: bg_entry_cat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: bg_entry_cat + table: bg_sub_cat + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: bg_cat + foreign_key_column: id + - collection: directus_users + field: username + type: string + meta: + collection: directus_users + conditions: null + display: raw + display_options: null + field: username + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: username + table: directus_users + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: true + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: id + type: integer + meta: + collection: hazard_danger + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: hazard_danger + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: sort + type: integer + meta: + collection: hazard_danger + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: hazard_danger + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: user_created + type: string + meta: + collection: hazard_danger + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 4 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: hazard_danger + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_danger + field: date_created + type: timestamp + meta: + collection: hazard_danger + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: hazard_danger + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: user_updated + type: string + meta: + collection: hazard_danger + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 6 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: hazard_danger + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_danger + field: date_updated + type: timestamp + meta: + collection: hazard_danger + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: hazard_danger + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: danger + type: string + meta: + collection: hazard_danger + conditions: null + display: null + display_options: null + field: danger + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: danger + table: hazard_danger + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_danger + field: description + type: string + meta: + collection: hazard_danger + conditions: null + display: null + display_options: null + field: description + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: description + table: hazard_danger + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: id + type: integer + meta: + collection: hazard_severity + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: hazard_severity + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: sort + type: integer + meta: + collection: hazard_severity + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: hazard_severity + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: user_created + type: string + meta: + collection: hazard_severity + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: hazard_severity + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_severity + field: date_created + type: timestamp + meta: + collection: hazard_severity + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: hazard_severity + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: user_updated + type: string + meta: + collection: hazard_severity + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 7 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: hazard_severity + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_severity + field: date_updated + type: timestamp + meta: + collection: hazard_severity + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 8 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: hazard_severity + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: severity + type: string + meta: + collection: hazard_severity + conditions: null + display: null + display_options: null + field: severity + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: severity + table: hazard_severity + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_severity + field: description + type: string + meta: + collection: hazard_severity + conditions: null + display: null + display_options: null + field: description + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: description + table: hazard_severity + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: id + type: integer + meta: + collection: hazard_source + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: hazard_source + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: sort + type: integer + meta: + collection: hazard_source + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: hazard_source + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: user_created + type: string + meta: + collection: hazard_source + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 4 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: hazard_source + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_source + field: date_created + type: timestamp + meta: + collection: hazard_source + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: hazard_source + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: user_updated + type: string + meta: + collection: hazard_source + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 6 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: hazard_source + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_source + field: date_updated + type: timestamp + meta: + collection: hazard_source + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: hazard_source + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: source + type: string + meta: + collection: hazard_source + conditions: null + display: formatted-value + display_options: null + field: source + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: source + table: hazard_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_source + field: description + type: string + meta: + collection: hazard_source + conditions: null + display: null + display_options: null + field: description + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: description + table: hazard_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_tags + field: id + type: integer + meta: + collection: hazard_tags + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: hazard_tags + field: sort + type: integer + meta: + collection: hazard_tags + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: hazard_tags + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_tags + field: user_created + type: string + meta: + collection: hazard_tags + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 6 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: hazard_tags + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_tags + field: date_created + type: timestamp + meta: + collection: hazard_tags + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: hazard_tags + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_tags + field: user_updated + type: string + meta: + collection: hazard_tags + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 8 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: hazard_tags + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: hazard_tags + field: date_updated + type: timestamp + meta: + collection: hazard_tags + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 9 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: hazard_tags + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: hazard_tags + field: hazard_source + type: integer + meta: + collection: hazard_tags + conditions: null + display: related-values + display_options: + template: '{{source}}' + field: hazard_source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{source}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: hazard_source + table: hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: hazard_source + foreign_key_column: id + - collection: hazard_tags + field: hazard_danger + type: integer + meta: + collection: hazard_tags + conditions: null + display: related-values + display_options: + template: '{{danger}}' + field: hazard_danger + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{danger}}' + readonly: false + required: false + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: hazard_danger + table: hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: hazard_danger + foreign_key_column: id + - collection: hazard_tags + field: hazard_severity + type: integer + meta: + collection: hazard_tags + conditions: null + display: related-values + display_options: + template: '{{severity}}' + field: hazard_severity + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{severity}}' + readonly: false + required: false + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: hazard_severity + table: hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: hazard_severity + foreign_key_column: id + - collection: laser_focus_lens + field: id + type: integer + meta: + collection: laser_focus_lens + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_focus_lens + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens + field: sort + type: integer + meta: + collection: laser_focus_lens + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_focus_lens + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens + field: user_created + type: string + meta: + collection: laser_focus_lens + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_focus_lens + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens + field: date_created + type: timestamp + meta: + collection: laser_focus_lens + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_focus_lens + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens + field: user_updated + type: string + meta: + collection: laser_focus_lens + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_focus_lens + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens + field: date_updated + type: timestamp + meta: + collection: laser_focus_lens + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_focus_lens + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens + field: name + type: string + meta: + collection: laser_focus_lens + conditions: null + display: formatted-value + display_options: + suffix: mm + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_focus_lens + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_config + field: id + type: integer + meta: + collection: laser_focus_lens_config + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_focus_lens_config + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_config + field: sort + type: integer + meta: + collection: laser_focus_lens_config + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_focus_lens_config + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_config + field: user_created + type: string + meta: + collection: laser_focus_lens_config + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_focus_lens_config + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens_config + field: date_created + type: timestamp + meta: + collection: laser_focus_lens_config + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_focus_lens_config + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_config + field: user_updated + type: string + meta: + collection: laser_focus_lens_config + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_focus_lens_config + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens_config + field: date_updated + type: timestamp + meta: + collection: laser_focus_lens_config + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_focus_lens_config + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_config + field: name + type: string + meta: + collection: laser_focus_lens_config + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_focus_lens_config + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_diameter + field: id + type: integer + meta: + collection: laser_focus_lens_diameter + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_focus_lens_diameter + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_diameter + field: sort + type: integer + meta: + collection: laser_focus_lens_diameter + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_focus_lens_diameter + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_diameter + field: user_created + type: string + meta: + collection: laser_focus_lens_diameter + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_focus_lens_diameter + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens_diameter + field: date_created + type: timestamp + meta: + collection: laser_focus_lens_diameter + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_focus_lens_diameter + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_diameter + field: user_updated + type: string + meta: + collection: laser_focus_lens_diameter + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_focus_lens_diameter + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_focus_lens_diameter + field: date_updated + type: timestamp + meta: + collection: laser_focus_lens_diameter + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_focus_lens_diameter + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_focus_lens_diameter + field: name + type: string + meta: + collection: laser_focus_lens_diameter + conditions: null + display: formatted-value + display_options: + suffix: mm + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_focus_lens_diameter + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: id + type: integer + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_scan_lens + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: user_updated + type: string + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: laser_scan_lens + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: user_created + type: string + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: laser_scan_lens + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: sort + type: integer + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_scan_lens + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: date_created + type: timestamp + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: laser_scan_lens + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: date_updated + type: timestamp + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: laser_scan_lens + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: field_size + type: string + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: field_size + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: field_size + table: laser_scan_lens + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens + field: focal_length + type: string + meta: + collection: laser_scan_lens + conditions: null + display: null + display_options: null + field: focal_length + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: focal_length + table: laser_scan_lens + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_apt + field: id + type: integer + meta: + collection: laser_scan_lens_apt + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_scan_lens_apt + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_apt + field: sort + type: integer + meta: + collection: laser_scan_lens_apt + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_scan_lens_apt + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_apt + field: user_created + type: string + meta: + collection: laser_scan_lens_apt + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_scan_lens_apt + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_apt + field: date_created + type: timestamp + meta: + collection: laser_scan_lens_apt + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_scan_lens_apt + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_apt + field: user_updated + type: string + meta: + collection: laser_scan_lens_apt + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_scan_lens_apt + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_apt + field: date_updated + type: timestamp + meta: + collection: laser_scan_lens_apt + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_scan_lens_apt + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_apt + field: name + type: string + meta: + collection: laser_scan_lens_apt + conditions: null + display: formatted-value + display_options: + suffix: mm + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_scan_lens_apt + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_config + field: id + type: integer + meta: + collection: laser_scan_lens_config + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_scan_lens_config + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_config + field: sort + type: integer + meta: + collection: laser_scan_lens_config + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_scan_lens_config + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_config + field: user_created + type: string + meta: + collection: laser_scan_lens_config + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_scan_lens_config + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_config + field: date_created + type: timestamp + meta: + collection: laser_scan_lens_config + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_scan_lens_config + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_config + field: user_updated + type: string + meta: + collection: laser_scan_lens_config + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_scan_lens_config + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_config + field: date_updated + type: timestamp + meta: + collection: laser_scan_lens_config + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_scan_lens_config + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_config + field: name + type: string + meta: + collection: laser_scan_lens_config + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_scan_lens_config + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_exp + field: id + type: integer + meta: + collection: laser_scan_lens_exp + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_scan_lens_exp + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_exp + field: sort + type: integer + meta: + collection: laser_scan_lens_exp + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_scan_lens_exp + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_exp + field: user_created + type: string + meta: + collection: laser_scan_lens_exp + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: laser_scan_lens_exp + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_exp + field: date_created + type: timestamp + meta: + collection: laser_scan_lens_exp + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: laser_scan_lens_exp + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_exp + field: user_updated + type: string + meta: + collection: laser_scan_lens_exp + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: laser_scan_lens_exp + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: laser_scan_lens_exp + field: date_updated + type: timestamp + meta: + collection: laser_scan_lens_exp + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: laser_scan_lens_exp + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_scan_lens_exp + field: name + type: string + meta: + collection: laser_scan_lens_exp + conditions: null + display: formatted-value + display_options: + suffix: x + field: name + group: null + hidden: false + interface: input + note: null + options: + placeholder: '8' + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_scan_lens_exp + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: name + type: string + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: + placeholder: Lightburn + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: laser_software + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: user_updated + type: string + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: laser_software + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: user_created + type: string + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: laser_software + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: id + type: integer + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: laser_software + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: sort + type: integer + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: laser_software + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: date_created + type: timestamp + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: datetime + note: null + options: + relative: true + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: laser_software + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: date_updated + type: timestamp + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: datetime + note: null + options: + relative: true + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: laser_software + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: vendor + type: string + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: vendor + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: vendor + table: laser_software + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: type + type: string + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: type + group: null + hidden: false + interface: input + note: null + options: + placeholder: oem + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: type + table: laser_software + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_software + field: notes + type: text + meta: + collection: laser_software + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: laser_software + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: submission_id + type: integer + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 35 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: laser_source + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: op + type: string + meta: + collection: laser_source + conditions: null + display: labels + display_options: + choices: + - text: MOPA + value: pm + - text: Q-Switch + value: pq + format: false + field: op + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: MOPA + value: pm + - text: Q-Switch + value: pq + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: op + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: mj + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' mJ' + field: mj + group: null + hidden: false + interface: input + note: null + options: + placeholder: '1.5' + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mj + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: w + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' W' + field: w + group: null + hidden: false + interface: input + note: null + options: + placeholder: '60' + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: w + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: ns + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' ns' + field: ns + group: null + hidden: false + interface: input + note: null + options: + placeholder: 2 ◅ 500 + readonly: false + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: ns + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: kHz + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' kHz' + field: kHz + group: null + hidden: false + interface: input + note: null + options: + placeholder: 1 ◅ 4000 + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: kHz + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: make + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: make + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: make + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: instability + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: instability + group: null + hidden: false + interface: input + note: null + options: + placeholder: <5 + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: instability + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: v + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' V' + field: v + group: null + hidden: false + interface: input + note: null + options: + placeholder: '48' + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: v + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: nm + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' nm' + field: nm + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: nm + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: band + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: band + group: null + hidden: false + interface: input + note: null + options: + placeholder: <15 @ 3dB + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: band + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: polarization + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: polarization + group: null + hidden: false + interface: input + note: null + options: + placeholder: random + readonly: false + required: false + searchable: true + sort: 15 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: polarization + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: d + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' mm' + field: d + group: null + hidden: false + interface: input + note: null + options: + placeholder: 7±1 + readonly: false + required: false + searchable: true + sort: 12 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: d + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: temp_op + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' °C' + field: temp_op + group: null + hidden: false + interface: input + note: null + options: + placeholder: 0 ◅ 40 + readonly: false + required: false + searchable: true + sort: 19 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: temp_op + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: temp_store + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' °C' + field: temp_store + group: null + hidden: false + interface: input + note: null + options: + placeholder: '-10 ◅ 60' + readonly: false + required: false + searchable: true + sort: 20 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: temp_store + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: cooling + type: string + meta: + collection: laser_source + conditions: null + display: labels + display_options: + choices: + - text: Air, Active + value: aa + - text: Air, Passive + value: ap + - text: Water + value: w + field: cooling + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: Air, Active + value: aa + - text: Air, Passive + value: ap + - text: Water + value: w + readonly: false + required: false + searchable: true + sort: 18 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: cooling + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: m2 + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: null + field: m2 + group: null + hidden: false + interface: input + note: null + options: + placeholder: <1.5 + readonly: false + required: false + searchable: true + sort: 14 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: m2 + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: submission_date + type: dateTime + meta: + collection: laser_source + conditions: null + display: datetime + display_options: + relative: true + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 36 + special: + - date-created + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: laser_source + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: last_modified_date + type: dateTime + meta: + collection: laser_source + conditions: null + display: datetime + display_options: + relative: true + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 37 + special: + - date-created + - date-updated + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: laser_source + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: cable + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' m' + field: cable + group: null + hidden: false + interface: input + note: null + options: + placeholder: '3' + readonly: false + required: false + searchable: true + sort: 29 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: cable + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: weight + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' kg' + field: weight + group: null + hidden: false + interface: input + note: null + options: + placeholder: '4.1' + readonly: false + required: false + searchable: true + sort: 30 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: weight + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: dimensions + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' mm' + field: dimensions + group: null + hidden: false + interface: input + note: null + options: + placeholder: 205 x 253.3 x 75 + readonly: false + required: false + searchable: true + sort: 33 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: dimensions + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: model + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: model + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: model + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: notes + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 34 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: mj_c + type: string + meta: + collection: laser_source + conditions: null + display: null + display_options: null + field: mj_c + group: null + hidden: false + interface: input + note: null + options: + placeholder: '@ 500ns 30kHz 30W' + readonly: false + required: false + searchable: true + sort: 23 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mj_c + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: d_c + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: d_c + group: null + hidden: false + interface: input + note: null + options: + placeholder: '@ 86% Power' + readonly: false + required: false + searchable: true + sort: 25 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: d_c + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: mw + type: string + meta: + collection: laser_source + conditions: null + display: formatted-value + display_options: + suffix: ' mW' + field: mw + group: null + hidden: false + interface: input + note: power of red dot pointer in mW + options: + placeholder: '0.5' + readonly: false + required: false + searchable: true + sort: 28 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mw + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: l_on + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: l_on + group: null + hidden: false + interface: input + note: null + options: + placeholder: 2 ◅ 20 ◅ 50 + readonly: false + required: false + searchable: true + sort: 21 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: l_on + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: l_off + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: l_off + group: null + hidden: false + interface: input + note: null + options: + placeholder: 2 ◅ 5 + readonly: false + required: false + searchable: true + sort: 22 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: l_off + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: on_c + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: on_c + group: null + hidden: false + interface: input + note: null + options: + placeholder: 0-90% Power + readonly: false + required: false + searchable: true + sort: 26 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: on_c + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: off_c + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: off_c + group: null + hidden: false + interface: input + note: null + options: + placeholder: 100-10% Power + readonly: false + required: false + searchable: true + sort: 27 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: off_c + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: anti + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: anti + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: 'yes' + value: 'yes' + - text: 'no' + value: 'no' + - text: unknown + value: unknown + readonly: false + required: false + searchable: true + sort: 17 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: anti + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: laser_source + field: ns_c + type: string + meta: + collection: laser_source + conditions: null + display: raw + display_options: null + field: ns_c + group: null + hidden: false + interface: input + note: null + options: + placeholder: '@ 30kHz' + readonly: false + required: false + searchable: true + sort: 24 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: ns_c + table: laser_source + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: user_updated + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 14 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: material + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: id + type: integer + meta: + collection: material + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 12 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material + field: sort + type: integer + meta: + collection: material + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 17 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: date_created + type: timestamp + meta: + collection: material + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 15 + special: + - date-created + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: material + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: date_updated + type: timestamp + meta: + collection: material + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 16 + special: + - date-created + - date-updated + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: material + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: name + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: abbreviation + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: abbreviation + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: abbreviation + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: technical_name + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: technical_name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: technical_name + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: composition + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: composition + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: composition + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: user_created + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: material + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: material_cat + type: integer + meta: + collection: material + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: material_cat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 1 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: material_cat + table: material + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_cat + foreign_key_column: id + - collection: material + field: material_status + type: integer + meta: + collection: material + conditions: null + display: related-values + display_options: + template: "{{danger}}\_{{name}}" + field: material_status + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: material_status + table: material + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_status + foreign_key_column: id + - collection: material + field: hazard_tags + type: alias + meta: + collection: material + conditions: null + display: related-values + display_options: + template: "{{hazard_tags_id.hazard_source.source}}\_| {{hazard_tags_id.hazard_danger.danger}}\_| {{hazard_tags_id.hazard_severity.severity}}" + field: hazard_tags + group: null + hidden: false + interface: list-m2m + note: null + options: + fields: + - hazard_tags_id.hazard_source.source + - hazard_tags_id.hazard_danger.danger + - hazard_tags_id.hazard_severity.severity + layout: table + tableSpacing: compact + readonly: false + required: false + searchable: true + sort: 4 + special: + - m2m + translations: null + validation: null + validation_message: null + width: full + - collection: material + field: notes + type: text + meta: + collection: material + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: material + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: material_status_override + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: material_status_override + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: CRITICAL RISK > DANGEROUS + value: cr>d + - text: DANGEROUS > CAUTION + value: d>c + - text: CAUTION > SAFE + value: c>s + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: material_status_override + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: override_reason + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: override_reason + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: override_reason + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material + field: common_names + type: string + meta: + collection: material + conditions: null + display: null + display_options: null + field: common_names + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: common_names + table: material + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: id + type: integer + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_cat + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: user_created + type: string + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: material_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: user_updated + type: string + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: material_cat + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: date_created + type: timestamp + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: material_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: date_updated + type: timestamp + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: material_cat + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: name + type: string + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: + placeholder: Textiles + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: material_cat + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_cat + field: sort + type: integer + meta: + collection: material_cat + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material_cat + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: id + type: integer + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_coating + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: sort + type: integer + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 15 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material_coating + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: user_created + type: string + meta: + collection: material_coating + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 11 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: material_coating + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: material_coating + field: date_created + type: timestamp + meta: + collection: material_coating + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 12 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: material_coating + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: user_updated + type: string + meta: + collection: material_coating + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 13 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: material_coating + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: material_coating + field: date_updated + type: timestamp + meta: + collection: material_coating + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 14 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: material_coating + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: abbreviation + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: abbreviation + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: abbreviation + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: technical_name + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: technical_name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: technical_name + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: composition + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: composition + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: composition + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: name + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: coating_status + type: integer + meta: + collection: material_coating + conditions: null + display: related-values + display_options: + template: "{{danger}}\_{{name}}" + field: coating_status + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{danger}}\_{{name}}" + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: coating_status + table: material_coating + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_status + foreign_key_column: id + - collection: material_coating + field: hazard_tags + type: alias + meta: + collection: material_coating + conditions: null + display: related-values + display_options: + template: "{{hazard_tags_id.hazard_source.source}}\_| {{hazard_tags_id.hazard_danger.danger}}\_| {{hazard_tags_id.hazard_severity.severity}}" + field: hazard_tags + group: null + hidden: false + interface: list-m2m + note: null + options: + fields: + - hazard_tags_id.hazard_source.source + - hazard_tags_id.hazard_danger.danger + - hazard_tags_id.hazard_severity.severity + layout: table + tableSpacing: compact + template: "{{hazard_tags_id.hazard_source.source}}\_| {{hazard_tags_id.hazard_danger.danger}}\_| {{hazard_tags_id.hazard_severity.severity}}" + readonly: false + required: false + searchable: true + sort: 3 + special: + - m2m + translations: null + validation: null + validation_message: null + width: full + - collection: material_coating + field: notes + type: text + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: material_coating + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: coating_status_override + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: coating_status_override + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: CRITICAL RISK > DANGEROUS + value: cr>d + - text: DANGEROUS > CAUTION + value: d>c + - text: CAUTION > SAFE + value: c>s + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: coating_status_override + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating + field: override_reason + type: string + meta: + collection: material_coating + conditions: null + display: null + display_options: null + field: override_reason + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: override_reason + table: material_coating + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_coating_hazard_tags + field: id + type: integer + meta: + collection: material_coating_hazard_tags + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_coating_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_coating_hazard_tags + field: material_coating_id + type: integer + meta: + collection: material_coating_hazard_tags + conditions: null + display: null + display_options: null + field: material_coating_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: material_coating_id + table: material_coating_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_coating + foreign_key_column: id + - collection: material_coating_hazard_tags + field: hazard_tags_id + type: integer + meta: + collection: material_coating_hazard_tags + conditions: null + display: null + display_options: null + field: hazard_tags_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: hazard_tags_id + table: material_coating_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: hazard_tags + foreign_key_column: id + - collection: material_color + field: id + type: integer + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_color + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: user_created + type: string + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: material_color + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: user_updated + type: string + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: material_color + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: sort + type: integer + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material_color + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: date_created + type: timestamp + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: material_color + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: date_updated + type: timestamp + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: material_color + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: name + type: string + meta: + collection: material_color + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: material_color + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_color + field: colors + type: string + meta: + collection: material_color + conditions: null + display: color + display_options: null + field: colors + group: null + hidden: false + interface: select-color + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: colors + table: material_color + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_hazard_tags + field: id + type: integer + meta: + collection: material_hazard_tags + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_hazard_tags + field: material_id + type: integer + meta: + collection: material_hazard_tags + conditions: null + display: null + display_options: null + field: material_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: material_id + table: material_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material + foreign_key_column: id + - collection: material_hazard_tags + field: hazard_tags_id + type: integer + meta: + collection: material_hazard_tags + conditions: null + display: null + display_options: null + field: hazard_tags_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: hazard_tags_id + table: material_hazard_tags + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: hazard_tags + foreign_key_column: id + - collection: material_opacity + field: id + type: integer + meta: + collection: material_opacity + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_opacity + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_opacity + field: sort + type: integer + meta: + collection: material_opacity + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material_opacity + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_opacity + field: user_created + type: string + meta: + collection: material_opacity + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 3 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: material_opacity + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: material_opacity + field: date_created + type: timestamp + meta: + collection: material_opacity + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: material_opacity + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_opacity + field: user_updated + type: string + meta: + collection: material_opacity + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 5 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: material_opacity + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: material_opacity + field: date_updated + type: timestamp + meta: + collection: material_opacity + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: material_opacity + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_opacity + field: opacity + type: string + meta: + collection: material_opacity + conditions: null + display: raw + display_options: + choices: null + field: opacity + group: null + hidden: false + interface: input + note: null + options: + choices: null + placeholder: Transparent + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: opacity + table: material_opacity + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: id + type: integer + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: material_status + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: user_created + type: string + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: user_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_created + table: material_status + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: user_updated + type: string + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: user_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: user_updated + table: material_status + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: sort + type: integer + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: material_status + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: date_created + type: timestamp + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_created + table: material_status + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: date_updated + type: timestamp + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: date_updated + table: material_status + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: name + type: string + meta: + collection: material_status + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: material_status + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: material_status + field: danger + type: string + meta: + collection: material_status + conditions: null + display: color + display_options: null + field: danger + group: null + hidden: false + interface: select-color + note: null + options: + presets: + - color: '#FFFFFF' + name: Safe + - color: '#F5C211' + name: Caution + - color: '#E66100' + name: Dangerous + - color: '#C01C28' + name: Critical Risk + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: danger + table: material_status + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: submission_id + type: integer + meta: + collection: projects + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: projects + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: title + type: string + meta: + collection: projects + conditions: null + display: formatted-value + display_options: + format: true + field: title + group: null + hidden: false + interface: input + note: null + options: + placeholder: ' My Awesome Project' + readonly: false + required: true + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: title + table: projects + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: uploader + type: string + meta: + collection: projects + conditions: null + display: formatted-value + display_options: null + field: uploader + group: null + hidden: false + interface: input + note: null + options: + placeholder: My Awesome Name + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: uploader + table: projects + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: category + type: string + meta: + collection: projects + conditions: null + display: formatted-value + display_options: + format: true + field: category + group: null + hidden: false + interface: select-dropdown + note: null + options: + allowNone: true + allowOther: true + choices: + - text: Assets + value: assets + - text: Documents + value: documents + - text: Fixtures + value: fixtures + - text: Projects + value: projects + - text: Templates + value: templates + - text: Test Files + value: test_files + - text: Tools + value: tools + readonly: false + required: true + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: category + table: projects + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: submission_date + type: dateTime + meta: + collection: projects + conditions: null + display: null + display_options: null + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: + - date-created + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: projects + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: last_modified_date + type: dateTime + meta: + collection: projects + conditions: null + display: null + display_options: null + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 10 + special: + - date-created + - date-updated + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: projects + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: tags + type: json + meta: + collection: projects + conditions: null + display: null + display_options: null + field: tags + group: null + hidden: false + interface: tags + note: null + options: + placeholder: Add some tags for discoverability! + presets: + - '1064' + - fiber + - '10600' + - co2 + - gantry + - galvo + - 3d printing + - acrylic + - wood + - metal + - '355' + - uv + - '455' + - diode + - assembly required + - beginner + - intermediate + - advanced + - air assist + - rotary + - business + - document + - lightburn + - clb + - lbrn2 + - zip + - eps + - ai + - svg + - jpg + - png + - xcf + - vector + - raster + - asset + - testing + - test grid + - ezcad + - ezd + - ez3 + - plastic + - synthetic + - natural + - diy + - branding + - 3d model + - printing + - plotter + - uv printing + - form + - art + - fixture + - project + - template + - tools + - reference + - outline + - sales + - marketing + - stl + readonly: false + required: false + searchable: true + sort: 11 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: tags + table: projects + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: p_files + type: alias + meta: + collection: projects + conditions: null + display: null + display_options: null + field: p_files + group: null + hidden: false + interface: files + note: null + options: + folder: f264f066-5b38-4335-bb10-5b014bfa62cb + readonly: false + required: true + searchable: true + sort: 7 + special: + - files + translations: null + validation: null + validation_message: null + width: full + - collection: projects + field: instructions + type: text + meta: + collection: projects + conditions: null + display: null + display_options: null + field: instructions + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 905a4259-0c8e-489b-b810-c27186a2f266 + placeholder: Instructions for your project? + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: instructions + table: projects + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: projects + field: p_image + type: uuid + meta: + collection: projects + conditions: null + display: image + display_options: null + field: p_image + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: da11b876-2ede-4e19-ad3a-76fc9db449a8 + readonly: false + required: true + searchable: true + sort: 6 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: p_image + table: projects + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: projects + field: owner + type: string + meta: + collection: projects + conditions: null + display: null + display_options: null + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: projects + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: projects_files + field: id + type: integer + meta: + collection: projects_files + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: projects_files + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: projects_files + field: projects_submission_id + type: integer + meta: + collection: projects_files + conditions: null + display: null + display_options: null + field: projects_submission_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: projects_submission_id + table: projects_files + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: projects + foreign_key_column: submission_id + - collection: projects_files + field: directus_files_id + type: string + meta: + collection: projects_files + conditions: null + display: null + display_options: null + field: directus_files_id + group: null + hidden: true + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: directus_files_id + table: projects_files + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_co2gal + field: setting_title + type: string + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + format: true + field: setting_title + group: null + hidden: false + interface: input + note: null + options: + placeholder: My Awesome Setting + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_title + table: settings_co2gal + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: uploader + type: string + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + format: true + field: uploader + group: null + hidden: false + interface: input + note: null + options: + placeholder: Mr Laser King + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: uploader + table: settings_co2gal + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: submission_date + type: dateTime + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 24 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: settings_co2gal + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: last_modified_date + type: dateTime + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 25 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: settings_co2gal + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: submission_id + type: integer + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 23 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: settings_co2gal + data_type: mediumint unsigned + default_value: null + max_length: null + numeric_precision: 8 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: photo + type: uuid + meta: + collection: settings_co2gal + conditions: null + display: image + display_options: null + field: photo + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: e5535371-828a-498b-80fc-3891b6220fd4 + readonly: false + required: true + searchable: true + sort: 4 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: photo + table: settings_co2gal + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_co2gal + field: screen + type: uuid + meta: + collection: settings_co2gal + conditions: null + display: image + display_options: null + field: screen + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: 8201e4c0-c39c-456a-bd55-1beb96642bcb + readonly: false + required: false + searchable: true + sort: 5 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: screen + table: settings_co2gal + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_co2gal + field: source + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: "{{make}}\_{{model}}" + field: source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{make}}\_{{model}}" + readonly: false + required: true + searchable: true + sort: 6 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: source + table: settings_co2gal + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_source + foreign_key_column: submission_id + - collection: settings_co2gal + field: lens + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: "{{field_size}}\_{{focal_length}}" + field: lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{field_size}}\_{{focal_length}}" + readonly: false + required: true + searchable: true + sort: 7 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens + foreign_key_column: id + - collection: settings_co2gal + field: focus + type: decimal + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + suffix: mm + field: focus + group: null + hidden: false + interface: input + note: Focus, + values focus away, - values focus closer, in (mm) + options: + alwaysShowValue: true + maxValue: 10 + minValue: -10 + placeholder: '0' + stepInterval: null + readonly: false + required: true + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: focus + table: settings_co2gal + data_type: decimal + default_value: 0 + max_length: null + numeric_precision: 3 + numeric_scale: 1 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: mat + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + field: mat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + readonly: false + required: true + searchable: true + sort: 12 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material + foreign_key_column: id + - collection: settings_co2gal + field: mat_coat + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + field: mat_coat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + readonly: false + required: true + searchable: true + sort: 13 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_coat + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_coating + foreign_key_column: id + - collection: settings_co2gal + field: mat_color + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: "{{colors}}\_{{name}}" + field: mat_color + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{colors}}\_{{name}}" + readonly: false + required: true + searchable: true + sort: 14 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_color + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_color + foreign_key_column: id + - collection: settings_co2gal + field: mat_opacity + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{opacity}}' + field: mat_opacity + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{opacity}}' + readonly: false + required: true + searchable: true + sort: 15 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_opacity + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_opacity + foreign_key_column: id + - collection: settings_co2gal + field: mat_thickness + type: decimal + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + suffix: mm + field: mat_thickness + group: null + hidden: false + interface: input + note: null + options: + placeholder: '3' + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_thickness + table: settings_co2gal + data_type: decimal + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 5 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: laser_soft + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: laser_soft + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 17 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_soft + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_software + foreign_key_column: id + - collection: settings_co2gal + field: repeat_all + type: integer + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + suffix: total passes + field: repeat_all + group: null + hidden: false + interface: input + note: How many times ALL settings below should be cycled through. + options: + max: 9999 + min: 1 + placeholder: '1' + readonly: false + required: true + searchable: true + sort: 18 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: repeat_all + table: settings_co2gal + data_type: int + default_value: 1 + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: setting_notes + type: text + meta: + collection: settings_co2gal + conditions: null + display: formatted-value + display_options: + format: true + field: setting_notes + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 7b04a706-754d-4302-a9a0-6c88cd8faddf + readonly: false + required: false + searchable: true + sort: 19 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_notes + table: settings_co2gal + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: fill_settings + type: json + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: fill_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Fill + fields: + - field: name + meta: + field: name + interface: input + options: + placeholder: Engraving Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: angle + meta: + display: formatted-value + display_options: + suffix: ° + field: angle + interface: input + note: Angle at which to execute the scan pattern in degrees + options: + placeholder: '45' + type: integer + width: half + name: angle + type: integer + - field: auto + meta: + display: boolean + field: auto + interface: boolean + note: Whether or not auto rotate is used, check for yes. + type: boolean + width: half + name: auto + type: boolean + - field: increment + meta: + display: formatted-value + display_options: + suffix: ° + field: increment + interface: input + note: >- + The angle per pass to adjust the scan pattern rotation when + using auto-rotate in degrees + options: + placeholder: '30' + type: integer + width: half + name: increment + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: flood + meta: + display: boolean + field: flood + interface: boolean + note: Whether or not flood fill is enabled, check for yes. + type: boolean + width: half + name: flood + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 20 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: fill_settings + table: settings_co2gal + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: line_settings + type: json + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: line_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Line + fields: + - field: name + meta: + display: formatted-value + display_options: + conditionalFormatting: null + format: true + field: name + interface: input + note: The name of the line setting + options: + placeholder: My great cut setting + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: perf + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: perf + interface: boolean + note: Is perforation enabled in your setting? + required: false + type: boolean + width: full + name: perf + type: boolean + - field: cut + meta: + display: formatted-value + display_options: + suffix: mm + field: cut + interface: input + note: Amount to cut before skip (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: cut + type: decimal + - field: skip + meta: + display: formatted-value + display_options: + suffix: mm + field: skip + interface: input + note: Amount to skip before the cut (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: skip + type: decimal + - field: wobble + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: wobble + interface: boolean + note: Is Wobble enabled in your setting? + type: boolean + width: full + name: wobble + type: boolean + - field: step + meta: + display: formatted-value + display_options: + suffix: mm + field: step + interface: input + note: Distance to step per wobble (mm) - wobble mode only. + options: + placeholder: '0.03' + type: decimal + width: half + name: step + type: decimal + - field: size + meta: + display: formatted-value + display_options: + suffix: mm + field: size + interface: input + note: Size to wobble per step (mm) - wobble mode only. + options: + placeholder: '0.2' + required: false + type: decimal + width: half + name: size + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 21 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: line_settings + table: settings_co2gal + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: raster_settings + type: json + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: raster_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Raster + fields: + - field: name + meta: + display: formatted-value + display_options: + format: true + field: name + interface: input + note: Name of the raster setting + options: + placeholder: Photo Cleanup Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: dither + meta: + field: dither + interface: select-dropdown + note: The dither mode to be applied to the raster image + options: + choices: + - text: Threshold + value: threshold + - text: Ordered + value: ordered + - text: Atkinson + value: atkinson + - text: Dither + value: dither + - text: Stucki + value: stucki + - text: Jarvis + value: jarvis + - text: Newsprint + value: newsprint + - text: Halftone + value: halftone + - text: Sketch + value: sketch + - text: Grayscale + value: grayscale + type: string + width: half + name: dither + type: string + - field: halftone_cell + meta: + display: formatted-value + field: halftone_cell + interface: input + note: The size of each halftone cell - only if halftone is selected + options: + placeholder: Cell size - halftone mode only. + type: decimal + width: half + name: halftone_cell + type: decimal + - field: halftone_angle + meta: + display: formatted-value + display_options: + suffix: ° + field: halftone_angle + interface: input + note: Angle the halftone pattern is applied to the image in degrees + options: + placeholder: Cell pattern angle - halftone mode only. + type: integer + width: half + name: halftone_angle + type: integer + - field: inversion + meta: + display: boolean + field: inversion + interface: boolean + note: Whether or not image inversion is applied, check for yes. + type: boolean + width: half + name: inversion + type: boolean + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: dot + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: dot + interface: input + note: >- + Adjustment to the width of a scan section to compensate for dot + overlap in mm + options: + placeholder: '0.08' + type: decimal + width: half + name: dot + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: angle + meta: + field: angle + interface: input + type: integer + width: half + name: angle + type: integer + - field: auto + meta: + field: auto + interface: boolean + type: boolean + width: half + name: auto + type: boolean + - field: increment + meta: + field: increment + interface: input + type: integer + width: half + name: increment + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 22 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: raster_settings + table: settings_co2gal + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: lens_conf + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: lens_conf + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 8 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens_conf + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens_config + foreign_key_column: id + - collection: settings_co2gal + field: lens_apt + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: lens_apt + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 9 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens_apt + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens_apt + foreign_key_column: id + - collection: settings_co2gal + field: lens_exp + type: integer + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: lens_exp + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 10 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens_exp + table: settings_co2gal + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens_exp + foreign_key_column: id + - collection: settings_co2gal + field: sort + type: integer + meta: + collection: settings_co2gal + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 26 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: settings_co2gal + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gal + field: owner + type: string + meta: + collection: settings_co2gal + conditions: null + display: related-values + display_options: + template: '{{username}}' + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: settings_co2gal + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: settings_co2gan + field: submission_id + type: integer + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 21 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: settings_co2gan + data_type: mediumint unsigned + default_value: null + max_length: null + numeric_precision: 8 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: setting_title + type: string + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + format: true + field: setting_title + group: null + hidden: false + interface: input + note: null + options: + placeholder: My Awesome Setting + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_title + table: settings_co2gan + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: uploader + type: string + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + format: true + field: uploader + group: null + hidden: false + interface: input + note: null + options: + placeholder: Mr Laser King + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: uploader + table: settings_co2gan + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: submission_date + type: dateTime + meta: + collection: settings_co2gan + conditions: null + display: datetime + display_options: + relative: true + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 22 + special: + - date-created + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: settings_co2gan + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: last_modified_date + type: dateTime + meta: + collection: settings_co2gan + conditions: null + display: datetime + display_options: + relative: true + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 23 + special: + - date-created + - date-updated + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: settings_co2gan + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: screen + type: uuid + meta: + collection: settings_co2gan + conditions: null + display: image + display_options: null + field: screen + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: 9b7d0b47-c1f4-4749-8876-2e4b52ccded0 + readonly: false + required: false + searchable: true + sort: 5 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: screen + table: settings_co2gan + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_co2gan + field: source + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: "{{make}}\_{{model}}" + field: source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{make}}\_{{model}}" + readonly: false + required: true + searchable: true + sort: 6 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: source + table: settings_co2gan + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_source + foreign_key_column: submission_id + - collection: settings_co2gan + field: focus + type: decimal + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + suffix: mm + field: focus + group: null + hidden: false + interface: input + note: Focus, + values focus away, - values focus closer, in (mm) + options: + alwaysShowValue: true + maxValue: 10 + minValue: -10 + placeholder: '0' + stepInterval: null + readonly: false + required: true + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: focus + table: settings_co2gan + data_type: decimal + default_value: 0 + max_length: null + numeric_precision: 3 + numeric_scale: 1 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: mat_thickness + type: decimal + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + suffix: mm + field: mat_thickness + group: null + hidden: false + interface: input + note: null + options: + placeholder: '3' + readonly: false + required: false + searchable: true + sort: 14 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_thickness + table: settings_co2gan + data_type: decimal + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 5 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: mat + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + field: mat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + readonly: false + required: true + searchable: true + sort: 10 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material + foreign_key_column: id + - collection: settings_co2gan + field: mat_coat + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + field: mat_coat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + readonly: false + required: true + searchable: true + sort: 11 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_coat + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_coating + foreign_key_column: id + - collection: settings_co2gan + field: mat_color + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: "{{colors}}\_{{name}}" + field: mat_color + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{colors}}\_{{name}}" + readonly: false + required: true + searchable: true + sort: 12 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_color + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_color + foreign_key_column: id + - collection: settings_co2gan + field: mat_opacity + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: '{{opacity}}' + field: mat_opacity + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{opacity}}' + readonly: false + required: true + searchable: true + sort: 13 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_opacity + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_opacity + foreign_key_column: id + - collection: settings_co2gan + field: laser_soft + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: laser_soft + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 15 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_soft + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_software + foreign_key_column: id + - collection: settings_co2gan + field: repeat_all + type: integer + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + suffix: total passes + field: repeat_all + group: null + hidden: false + interface: input + note: How many times ALL settings below should be cycled through. + options: + max: 9999 + min: 1 + placeholder: '1' + readonly: false + required: true + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: repeat_all + table: settings_co2gan + data_type: int + default_value: 1 + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: setting_notes + type: text + meta: + collection: settings_co2gan + conditions: null + display: formatted-value + display_options: + format: true + field: setting_notes + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 926e2c1a-7907-4ef2-b778-859c6f40ba82 + readonly: false + required: false + searchable: true + sort: 17 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_notes + table: settings_co2gan + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: fill_settings + type: json + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: fill_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Fill + fields: + - field: name + meta: + field: name + interface: input + options: + placeholder: Engraving Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: flood + meta: + display: boolean + field: flood + interface: boolean + note: Whether or not flood fill is enabled, check for yes. + type: boolean + width: half + name: flood + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 18 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: fill_settings + table: settings_co2gan + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: line_settings + type: json + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: line_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Line + fields: + - field: name + meta: + display: formatted-value + display_options: + conditionalFormatting: null + format: true + field: name + interface: input + note: The name of the line setting + options: + placeholder: My great cut setting + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: perf + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: perf + interface: boolean + note: Is perforation enabled in your setting? + required: false + type: boolean + width: full + name: perf + type: boolean + - field: cut + meta: + display: formatted-value + display_options: + suffix: mm + field: cut + interface: input + note: Amount to cut before skip (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: cut + type: decimal + - field: skip + meta: + display: formatted-value + display_options: + suffix: mm + field: skip + interface: input + note: Amount to skip before the cut (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: skip + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 19 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: line_settings + table: settings_co2gan + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: raster_settings + type: json + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: raster_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Raster + fields: + - field: name + meta: + display: formatted-value + display_options: + format: true + field: name + interface: input + note: Name of the raster setting + options: + placeholder: Photo Cleanup Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: dither + meta: + field: dither + interface: select-dropdown + note: The dither mode to be applied to the raster image + options: + choices: + - text: Threshold + value: threshold + - text: Ordered + value: ordered + - text: Atkinson + value: atkinson + - text: Dither + value: dither + - text: Stucki + value: stucki + - text: Jarvis + value: jarvis + - text: Newsprint + value: newsprint + - text: Halftone + value: halftone + - text: Sketch + value: sketch + - text: Grayscale + value: grayscale + type: string + width: half + name: dither + type: string + - field: halftone_cell + meta: + display: formatted-value + field: halftone_cell + interface: input + note: The size of each halftone cell - only if halftone is selected + options: + placeholder: Cell size - halftone mode only. + type: decimal + width: half + name: halftone_cell + type: decimal + - field: halftone_angle + meta: + display: formatted-value + display_options: + suffix: ° + field: halftone_angle + interface: input + note: Angle the halftone pattern is applied to the image in degrees + options: + placeholder: Cell pattern angle - halftone mode only. + type: integer + width: half + name: halftone_angle + type: integer + - field: inversion + meta: + display: boolean + field: inversion + interface: boolean + note: Whether or not image inversion is applied, check for yes. + type: boolean + width: half + name: inversion + type: boolean + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: dot + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: dot + interface: input + note: >- + Adjustment to the width of a scan section to compensate for dot + overlap in mm + options: + placeholder: '0.08' + type: decimal + width: half + name: dot + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 20 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: raster_settings + table: settings_co2gan + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: photo + type: uuid + meta: + collection: settings_co2gan + conditions: null + display: image + display_options: null + field: photo + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: d19c4f8d-a42f-422d-b113-b89b736c34e6 + readonly: false + required: true + searchable: true + sort: 4 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: photo + table: settings_co2gan + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_co2gan + field: sort + type: integer + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 24 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: settings_co2gan + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_co2gan + field: lens + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 7 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_focus_lens + foreign_key_column: id + - collection: settings_co2gan + field: lens_conf + type: integer + meta: + collection: settings_co2gan + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: lens_conf + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 8 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens_conf + table: settings_co2gan + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_focus_lens_config + foreign_key_column: id + - collection: settings_co2gan + field: owner + type: string + meta: + collection: settings_co2gan + conditions: null + display: null + display_options: null + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: settings_co2gan + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: settings_fiber + field: submission_id + type: integer + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 20 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: settings_fiber + data_type: mediumint unsigned + default_value: null + max_length: null + numeric_precision: 8 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: setting_title + type: string + meta: + collection: settings_fiber + conditions: null + display: formatted-value + display_options: + format: true + field: setting_title + group: null + hidden: false + interface: input + note: null + options: + placeholder: My Awesome Setting + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_title + table: settings_fiber + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: uploader + type: string + meta: + collection: settings_fiber + conditions: null + display: formatted-value + display_options: + format: true + field: uploader + group: null + hidden: false + interface: input + note: null + options: + placeholder: Mr Laser King + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: uploader + table: settings_fiber + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: submission_date + type: dateTime + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 21 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: settings_fiber + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: last_modified_date + type: dateTime + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 22 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: settings_fiber + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: photo + type: uuid + meta: + collection: settings_fiber + conditions: null + display: image + display_options: null + field: photo + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: 54f6a9d2-bc57-41fc-8c7d-7c7d7cb9cadc + readonly: false + required: true + searchable: true + sort: 4 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: photo + table: settings_fiber + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_fiber + field: screen + type: uuid + meta: + collection: settings_fiber + conditions: null + display: image + display_options: null + field: screen + group: null + hidden: false + interface: file-image + note: null + options: + folder: 5c830975-7926-4e01-911c-2443b62d7f88 + readonly: false + required: false + searchable: true + sort: 5 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: screen + table: settings_fiber + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_fiber + field: source + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: "{{make}}\_{{model}}" + field: source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{make}}\_{{model}}" + readonly: false + required: true + searchable: true + sort: 6 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: source + table: settings_fiber + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_source + foreign_key_column: submission_id + - collection: settings_fiber + field: lens + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: "{{field_size}}\_{{focal_length}}" + field: lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{field_size}}\_{{focal_length}}" + readonly: false + required: true + searchable: true + sort: 7 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens + foreign_key_column: id + - collection: settings_fiber + field: mat + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: "{{name}} \_{{material_status.danger}}\_{{material_status.name}}" + field: mat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + readonly: false + required: true + searchable: true + sort: 9 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material + foreign_key_column: id + - collection: settings_fiber + field: mat_color + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: "{{colors}} \_{{name}}" + field: mat_color + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{colors}}\_ {{name}}" + readonly: false + required: true + searchable: true + sort: 11 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_color + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_color + foreign_key_column: id + - collection: settings_fiber + field: mat_thickness + type: decimal + meta: + collection: settings_fiber + conditions: null + display: formatted-value + display_options: + suffix: mm + field: mat_thickness + group: null + hidden: false + interface: input + note: null + options: + placeholder: '3' + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_thickness + table: settings_fiber + data_type: decimal + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 5 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: laser_soft + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: laser_soft + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 14 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_soft + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_software + foreign_key_column: id + - collection: settings_fiber + field: fill_settings + type: json + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: fill_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Fill + fields: + - field: name + meta: + field: name + interface: input + options: + placeholder: Engraving Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: angle + meta: + display: formatted-value + display_options: + suffix: ° + field: angle + interface: input + note: Angle at which to execute the scan pattern in degrees + options: + placeholder: '45' + type: integer + width: half + name: angle + type: integer + - field: auto + meta: + display: boolean + field: auto + interface: boolean + note: Whether or not auto rotate is used, check for yes. + type: boolean + width: half + name: auto + type: boolean + - field: increment + meta: + display: formatted-value + display_options: + suffix: ° + field: increment + interface: input + note: >- + The angle per pass to adjust the scan pattern rotation when + using auto-rotate in degrees + options: + placeholder: '30' + type: integer + width: half + name: increment + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: flood + meta: + display: boolean + field: flood + interface: boolean + note: Whether or not flood fill is enabled, check for yes. + type: boolean + width: half + name: flood + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 17 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: fill_settings + table: settings_fiber + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: line_settings + type: json + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: line_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Line + fields: + - field: name + meta: + display: formatted-value + display_options: + conditionalFormatting: null + format: true + field: name + interface: input + note: The name of the line setting + options: + placeholder: My great cut setting + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: perf + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: perf + interface: boolean + note: Is perforation enabled in your setting? + required: false + type: boolean + width: full + name: perf + type: boolean + - field: cut + meta: + display: formatted-value + display_options: + suffix: mm + field: cut + interface: input + note: Amount to cut before skip (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: cut + type: decimal + - field: skip + meta: + display: formatted-value + display_options: + suffix: mm + field: skip + interface: input + note: Amount to skip before the cut (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: skip + type: decimal + - field: wobble + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: wobble + interface: boolean + note: Is Wobble enabled in your setting? + type: boolean + width: full + name: wobble + type: boolean + - field: step + meta: + display: formatted-value + display_options: + suffix: mm + field: step + interface: input + note: Distance to step per wobble (mm) - wobble mode only. + options: + placeholder: '0.03' + type: decimal + width: half + name: step + type: decimal + - field: size + meta: + display: formatted-value + display_options: + suffix: mm + field: size + interface: input + note: Size to wobble per step (mm) - wobble mode only. + options: + placeholder: '0.2' + required: false + type: decimal + width: half + name: size + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 18 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: line_settings + table: settings_fiber + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: raster_settings + type: json + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: raster_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Raster + fields: + - field: name + meta: + display: formatted-value + display_options: + format: true + field: name + interface: input + note: Name of the raster setting + options: + placeholder: Photo Cleanup Pass + type: string + width: full + name: name + type: string + - field: power + meta: + display: formatted-value + display_options: + conditionalFormatting: null + suffix: ' %' + field: power + interface: input + note: Power as a % of total available. + options: + placeholder: '80' + type: integer + width: half + name: power + type: integer + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: dither + meta: + field: dither + interface: select-dropdown + note: The dither mode to be applied to the raster image + options: + choices: + - text: Threshold + value: threshold + - text: Ordered + value: ordered + - text: Atkinson + value: atkinson + - text: Dither + value: dither + - text: Stucki + value: stucki + - text: Jarvis + value: jarvis + - text: Newsprint + value: newsprint + - text: Halftone + value: halftone + - text: Sketch + value: sketch + - text: Grayscale + value: grayscale + type: string + width: half + name: dither + type: string + - field: halftone_cell + meta: + display: formatted-value + field: halftone_cell + interface: input + note: The size of each halftone cell - only if halftone is selected + options: + placeholder: Cell size - halftone mode only. + type: decimal + width: half + name: halftone_cell + type: decimal + - field: halftone_angle + meta: + display: formatted-value + display_options: + suffix: ° + field: halftone_angle + interface: input + note: Angle the halftone pattern is applied to the image in degrees + options: + placeholder: Cell pattern angle - halftone mode only. + type: integer + width: half + name: halftone_angle + type: integer + - field: inversion + meta: + display: boolean + field: inversion + interface: boolean + note: Whether or not image inversion is applied, check for yes. + type: boolean + width: half + name: inversion + type: boolean + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: dot + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: dot + interface: input + note: >- + Adjustment to the width of a scan section to compensate for dot + overlap in mm + options: + placeholder: '0.08' + type: decimal + width: half + name: dot + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 19 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: raster_settings + table: settings_fiber + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: mat_coat + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: "{{name}}\_ {{coating_status.danger}}\_{{coating_status.name}}" + field: mat_coat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{name}}\_ {{coating_status.danger}}\_{{coating_status.name}}" + readonly: false + required: true + searchable: true + sort: 10 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_coat + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_coating + foreign_key_column: id + - collection: settings_fiber + field: mat_opacity + type: integer + meta: + collection: settings_fiber + conditions: null + display: related-values + display_options: + template: '{{opacity}}' + field: mat_opacity + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{opacity}}' + readonly: false + required: true + searchable: true + sort: 12 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_opacity + table: settings_fiber + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_opacity + foreign_key_column: id + - collection: settings_fiber + field: repeat_all + type: integer + meta: + collection: settings_fiber + conditions: null + display: formatted-value + display_options: + suffix: total passes + field: repeat_all + group: null + hidden: false + interface: input + note: How many times ALL settings below should be cycled through. + options: + max: 9999 + min: 1 + placeholder: '1' + readonly: false + required: true + searchable: true + sort: 15 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: repeat_all + table: settings_fiber + data_type: int + default_value: 1 + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: setting_notes + type: text + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: setting_notes + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 00eed759-480e-43cc-9de3-854dc59cca79 + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_notes + table: settings_fiber + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: focus + type: decimal + meta: + collection: settings_fiber + conditions: null + display: formatted-value + display_options: + suffix: mm + field: focus + group: null + hidden: false + interface: input + note: Focus, + values focus away, - values focus closer, in (mm) + options: + alwaysShowValue: true + maxValue: 10 + minValue: -10 + placeholder: '0' + stepInterval: null + readonly: false + required: true + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: focus + table: settings_fiber + data_type: decimal + default_value: 0 + max_length: null + numeric_precision: 3 + numeric_scale: 1 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_fiber + field: owner + type: string + meta: + collection: settings_fiber + conditions: null + display: null + display_options: null + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: settings_fiber + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: settings_uv + field: uploader + type: string + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + format: true + field: uploader + group: null + hidden: false + interface: input + note: null + options: + placeholder: Mr Laser King + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: uploader + table: settings_uv + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: submission_date + type: dateTime + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: submission_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 21 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_date + table: settings_uv + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: last_modified_date + type: dateTime + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: last_modified_date + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 22 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_modified_date + table: settings_uv + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: submission_id + type: integer + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: submission_id + group: null + hidden: false + interface: null + note: null + options: null + readonly: false + required: false + searchable: true + sort: 20 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: submission_id + table: settings_uv + data_type: mediumint unsigned + default_value: null + max_length: null + numeric_precision: 8 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: setting_title + type: string + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + format: true + field: setting_title + group: null + hidden: false + interface: input + note: null + options: + placeholder: My Awesome Setting + readonly: false + required: true + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_title + table: settings_uv + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: screen + type: uuid + meta: + collection: settings_uv + conditions: null + display: image + display_options: null + field: screen + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: a84f54b1-0e92-4ea6-8fbe-37a3a74bd49c + readonly: false + required: false + searchable: true + sort: 5 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: screen + table: settings_uv + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_uv + field: photo + type: uuid + meta: + collection: settings_uv + conditions: null + display: image + display_options: null + field: photo + group: null + hidden: false + interface: file-image + note: null + options: + crop: false + folder: c639360b-3116-4b5d-98da-f8b502089486 + readonly: false + required: true + searchable: true + sort: 4 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: photo + table: settings_uv + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: settings_uv + field: source + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: "{{make}}\_{{model}}" + field: source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{make}}\_{{model}}" + readonly: false + required: true + searchable: true + sort: 6 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: source + table: settings_uv + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_source + foreign_key_column: submission_id + - collection: settings_uv + field: lens + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: "{{field_size}}\_{{focal_length}}" + field: lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{field_size}}\_{{focal_length}}" + readonly: false + required: true + searchable: true + sort: 7 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: lens + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens + foreign_key_column: id + - collection: settings_uv + field: focus + type: decimal + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + suffix: mm + field: focus + group: null + hidden: false + interface: input + note: Focus, + values focus away, - values focus closer, in (mm) + options: + alwaysShowValue: true + maxValue: 10 + minValue: -10 + placeholder: '0' + stepInterval: null + readonly: false + required: true + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: focus + table: settings_uv + data_type: decimal + default_value: 0 + max_length: null + numeric_precision: 3 + numeric_scale: 1 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: mat + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + field: mat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: "{{name}}\_\_\_\_ {{material_status.danger}}\_{{material_status.name}}" + readonly: false + required: true + searchable: true + sort: 9 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material + foreign_key_column: id + - collection: settings_uv + field: mat_coat + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + field: mat_coat + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{name}}\_\_\_\_ {{coating_status.danger}}\_{{coating_status.name}}" + readonly: false + required: true + searchable: true + sort: 10 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_coat + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_coating + foreign_key_column: id + - collection: settings_uv + field: mat_color + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: "{{colors}}\_{{name}}" + field: mat_color + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: "{{colors}}\_{{name}}" + readonly: false + required: true + searchable: true + sort: 11 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_color + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_color + foreign_key_column: id + - collection: settings_uv + field: mat_opacity + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: '{{opacity}}' + field: mat_opacity + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + enableCreate: false + template: '{{opacity}}' + readonly: false + required: true + searchable: true + sort: 12 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_opacity + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: material_opacity + foreign_key_column: id + - collection: settings_uv + field: mat_thickness + type: decimal + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + suffix: mm + field: mat_thickness + group: null + hidden: false + interface: input + note: null + options: + placeholder: '3' + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: mat_thickness + table: settings_uv + data_type: decimal + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 5 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: laser_soft + type: integer + meta: + collection: settings_uv + conditions: null + display: related-values + display_options: + template: '{{name}}' + field: laser_soft + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 14 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_soft + table: settings_uv + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_software + foreign_key_column: id + - collection: settings_uv + field: repeat_all + type: integer + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + suffix: total passes + field: repeat_all + group: null + hidden: false + interface: input + note: How many times ALL settings below should be cycled through. + options: + max: 9999 + min: 1 + placeholder: '1' + readonly: false + required: true + searchable: true + sort: 15 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: repeat_all + table: settings_uv + data_type: int + default_value: 1 + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: setting_notes + type: text + meta: + collection: settings_uv + conditions: null + display: formatted-value + display_options: + format: true + field: setting_notes + group: null + hidden: false + interface: input-rich-text-md + note: null + options: + folder: 8ca37379-7178-48b2-8670-6b8d8a880677 + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: setting_notes + table: settings_uv + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: fill_settings + type: json + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: fill_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Fill + fields: + - field: name + meta: + field: name + interface: input + options: + placeholder: Engraving Pass + type: string + width: full + name: name + type: string + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: angle + meta: + display: formatted-value + display_options: + suffix: ° + field: angle + interface: input + note: Angle at which to execute the scan pattern in degrees + options: + placeholder: '45' + type: integer + width: half + name: angle + type: integer + - field: auto + meta: + display: boolean + field: auto + interface: boolean + note: Whether or not auto rotate is used, check for yes. + type: boolean + width: half + name: auto + type: boolean + - field: increment + meta: + display: formatted-value + display_options: + suffix: ° + field: increment + interface: input + note: >- + The angle per pass to adjust the scan pattern rotation when + using auto-rotate in degrees + options: + placeholder: '30' + type: integer + width: half + name: increment + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: flood + meta: + display: boolean + field: flood + interface: boolean + note: Whether or not flood fill is enabled, check for yes. + type: boolean + width: half + name: flood + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 17 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: fill_settings + table: settings_uv + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: line_settings + type: json + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: line_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Line + fields: + - field: name + meta: + display: formatted-value + display_options: + conditionalFormatting: null + format: true + field: name + interface: input + note: The name of the line setting + options: + placeholder: My great cut setting + type: string + width: full + name: name + type: string + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: perf + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: perf + interface: boolean + note: Is perforation enabled in your setting? + required: false + type: boolean + width: full + name: perf + type: boolean + - field: cut + meta: + display: formatted-value + display_options: + suffix: mm + field: cut + interface: input + note: Amount to cut before skip (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: cut + type: decimal + - field: skip + meta: + display: formatted-value + display_options: + suffix: mm + field: skip + interface: input + note: Amount to skip before the cut (mm) - perf mode only. + options: + placeholder: '0.10' + type: decimal + width: half + name: skip + type: decimal + - field: wobble + meta: + display: boolean + display_options: + labelOff: Disabled + labelOn: Enabled + field: wobble + interface: boolean + note: Is Wobble enabled in your setting? + type: boolean + width: full + name: wobble + type: boolean + - field: step + meta: + display: formatted-value + display_options: + suffix: mm + field: step + interface: input + note: Distance to step per wobble (mm) - wobble mode only. + options: + placeholder: '0.03' + type: decimal + width: half + name: step + type: decimal + - field: size + meta: + display: formatted-value + display_options: + suffix: mm + field: size + interface: input + note: Size to wobble per step (mm) - wobble mode only. + options: + placeholder: '0.2' + required: false + type: decimal + width: half + name: size + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 18 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: line_settings + table: settings_uv + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: raster_settings + type: json + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: raster_settings + group: null + hidden: false + interface: list + note: null + options: + addLabel: Create New Raster + fields: + - field: name + meta: + display: formatted-value + display_options: + format: true + field: name + interface: input + note: Name of the raster setting + options: + placeholder: Photo Cleanup Pass + type: string + width: full + name: name + type: string + - field: speed + meta: + display: formatted-value + display_options: + suffix: ' mm/s' + field: speed + interface: input + note: Galvo scan speed in mm/s + options: + placeholder: '1000' + type: integer + width: half + name: speed + type: integer + - field: frequency + meta: + display: formatted-value + display_options: + suffix: ' kHz' + field: frequency + interface: input + note: Freqency of pulses in kHz + options: + placeholder: '45' + type: integer + width: half + name: frequency + type: integer + - field: pulse + meta: + display: formatted-value + display_options: + suffix: ' ns' + field: pulse + interface: input + note: Width of each pulse in ns + options: + placeholder: '200' + type: integer + width: half + name: pulse + type: integer + - field: type + meta: + display: formatted-value + display_options: + format: true + field: type + interface: select-dropdown + note: Scan pattern to execute + options: + choices: + - text: UniDirectional + value: uni + - text: BiDirectional + value: bi + - text: Offset Fill + value: offset + type: string + width: half + name: type + type: string + - field: dither + meta: + field: dither + interface: select-dropdown + note: The dither mode to be applied to the raster image + options: + choices: + - text: Threshold + value: threshold + - text: Ordered + value: ordered + - text: Atkinson + value: atkinson + - text: Dither + value: dither + - text: Stucki + value: stucki + - text: Jarvis + value: jarvis + - text: Newsprint + value: newsprint + - text: Halftone + value: halftone + - text: Sketch + value: sketch + - text: Grayscale + value: grayscale + type: string + width: half + name: dither + type: string + - field: halftone_cell + meta: + display: formatted-value + field: halftone_cell + interface: input + note: The size of each halftone cell - only if halftone is selected + options: + placeholder: Cell size - halftone mode only. + type: decimal + width: half + name: halftone_cell + type: decimal + - field: halftone_angle + meta: + display: formatted-value + display_options: + suffix: ° + field: halftone_angle + interface: input + note: Angle the halftone pattern is applied to the image in degrees + options: + placeholder: Cell pattern angle - halftone mode only. + type: integer + width: half + name: halftone_angle + type: integer + - field: inversion + meta: + display: boolean + field: inversion + interface: boolean + note: Whether or not image inversion is applied, check for yes. + type: boolean + width: half + name: inversion + type: boolean + - field: interval + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: interval + interface: input + note: Spacing between scan line centers in scan pattern in mm + options: + placeholder: '0.025' + type: decimal + width: half + name: interval + type: decimal + - field: dot + meta: + display: formatted-value + display_options: + suffix: ' mm' + field: dot + interface: input + note: >- + Adjustment to the width of a scan section to compensate for dot + overlap in mm + options: + placeholder: '0.08' + type: decimal + width: half + name: dot + type: decimal + - field: pass + meta: + display: formatted-value + display_options: + suffix: ' pass(es)' + field: pass + interface: input + note: Number of passes to execute + options: + placeholder: '1' + type: integer + width: half + name: pass + type: integer + - field: cross + meta: + display: boolean + field: cross + interface: boolean + note: Whether or not cross-hatch is enabled, check for yes. + type: boolean + width: half + name: cross + type: boolean + - field: air + meta: + display: boolean + field: air + interface: boolean + note: Whether or not air assist is enabled, check for yes. + type: boolean + width: half + name: air + type: boolean + readonly: false + required: false + searchable: true + sort: 19 + special: + - cast-json + translations: null + validation: null + validation_message: null + width: full + schema: + name: raster_settings + table: settings_uv + data_type: longtext + default_value: null + max_length: 4294967295 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: settings_uv + field: owner + type: string + meta: + collection: settings_uv + conditions: null + display: null + display_options: null + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: settings_uv + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_claims + field: id + type: integer + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: user_claims + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: status + type: string + meta: + collection: user_claims + conditions: null + display: labels + display_options: + choices: + - background: var(--theme--primary-background) + color: '#F9F06B' + foreground: var(--theme--primary) + text: pending + value: pending + - background: var(--theme--background-normal) + color: '#8FF0A4' + foreground: var(--theme--foreground) + text: approved + value: approved + - background: var(--theme--warning-background) + color: '#E01B24' + foreground: var(--theme--warning) + text: rejected + value: rejected + showAsDot: true + field: status + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - color: '#F9F06B' + text: pending + value: pending + - color: '#2EC27E' + text: approved + value: approved + - color: '#E01B24' + text: rejected + value: rejected + readonly: false + required: true + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: status + table: user_claims + data_type: varchar + default_value: pending + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: sort + type: integer + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: user_claims + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: user_created + type: string + meta: + collection: user_claims + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 7 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: user_claims + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_claims + field: date_created + type: timestamp + meta: + collection: user_claims + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 8 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: user_claims + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: user_updated + type: string + meta: + collection: user_claims + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 9 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: user_claims + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_claims + field: date_updated + type: timestamp + meta: + collection: user_claims + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 10 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: user_claims + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: target_id + type: string + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: target_id + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: target_id + table: user_claims + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: claimant + type: string + meta: + collection: user_claims + conditions: null + display: user + display_options: null + field: claimant + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: true + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: claimant + table: user_claims + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_claims + field: note + type: text + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: note + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: note + table: user_claims + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: reviewed_by + type: string + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: reviewed_by + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 12 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: reviewed_by + table: user_claims + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_claims + field: reviewed_at + type: dateTime + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: reviewed_at + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: reviewed_at + table: user_claims + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: target_collection + type: string + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: target_collection + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - color: '#8FF0A4' + text: settings_fiber + value: settings_fiber + - color: '#93006A' + text: settings_uv + value: settings_uv + - color: '#F66151' + text: settings_co2gal + value: settings_co2gal + - color: '#E01B24' + text: settings_co2gan + value: settings_co2gan + - color: '#813D9C' + text: projects + value: projects + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: target_collection + table: user_claims + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_claims + field: unique_key + type: string + meta: + collection: user_claims + conditions: null + display: null + display_options: null + field: unique_key + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 14 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: unique_key + table: user_claims + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: id + type: integer + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: user_memberships + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: provider + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: provider + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: Ko-Fi + value: kofi + - text: Patreon + value: patreon + - text: LMA + value: mighty + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: provider + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: external_user_id + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: external_user_id + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: external_user_id + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: email + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: email + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: email + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: username + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: username + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: username + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: status + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: status + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: active + value: active + - text: canceled + value: canceled + - text: refunded + value: refunded + - text: one-time + value: one_time + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: status + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: tier + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: tier + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: tier + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: started_at + type: dateTime + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: started_at + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: started_at + table: user_memberships + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: renews_at + type: dateTime + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: renews_at + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + searchable: true + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: renews_at + table: user_memberships + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: last_event_at + type: dateTime + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: last_event_at + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: last_event_at + table: user_memberships + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: app_user + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: app_user + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: app_user + table: user_memberships + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_memberships + field: claim_token + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: claim_token + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 13 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: claim_token + table: user_memberships + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: claim_expires_at + type: dateTime + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: claim_expires_at + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + searchable: true + sort: 14 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: claim_expires_at + table: user_memberships + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_memberships + field: claim_user_id + type: string + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: claim_user_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{username}}' + readonly: false + required: false + searchable: true + sort: 15 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: claim_user_id + table: user_memberships + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_memberships + field: raw + type: text + meta: + collection: user_memberships + conditions: null + display: null + display_options: null + field: raw + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 16 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: raw + table: user_memberships + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_preferences + field: id + type: integer + meta: + collection: user_preferences + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: user_preferences + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: user_preferences + field: status + type: string + meta: + collection: user_preferences + conditions: null + display: labels + display_options: + choices: + - background: var(--theme--primary-background) + color: var(--theme--primary) + foreground: var(--theme--primary) + text: $t:published + value: published + - background: var(--theme--background-normal) + color: var(--theme--foreground) + foreground: var(--theme--foreground) + text: $t:draft + value: draft + - background: var(--theme--warning-background) + color: var(--theme--warning) + foreground: var(--theme--warning) + text: $t:archived + value: archived + showAsDot: true + field: status + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - color: var(--theme--primary) + text: $t:published + value: published + - color: var(--theme--foreground) + text: $t:draft + value: draft + - color: var(--theme--warning) + text: $t:archived + value: archived + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: status + table: user_preferences + data_type: varchar + default_value: draft + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_preferences + field: sort + type: integer + meta: + collection: user_preferences + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: user_preferences + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_preferences + field: user_created + type: string + meta: + collection: user_preferences + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 4 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: user_preferences + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_preferences + field: date_created + type: timestamp + meta: + collection: user_preferences + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: user_preferences + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_preferences + field: user_updated + type: string + meta: + collection: user_preferences + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 6 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: user_preferences + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_preferences + field: date_updated + type: timestamp + meta: + collection: user_preferences + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: user_preferences + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rig_type + field: id + type: integer + meta: + collection: user_rig_type + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: user_rig_type + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: user_rig_type + field: sort + type: integer + meta: + collection: user_rig_type + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: user_rig_type + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rig_type + field: name + type: string + meta: + collection: user_rig_type + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: user_rig_type + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: id + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: sort + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: sort + group: null + hidden: true + interface: input + note: null + options: null + readonly: false + required: false + searchable: true + sort: 12 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: sort + table: user_rigs + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: user_created + type: string + meta: + collection: user_rigs + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 13 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: user_rigs + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_rigs + field: date_created + type: timestamp + meta: + collection: user_rigs + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 14 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: user_rigs + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: user_updated + type: string + meta: + collection: user_rigs + conditions: null + display: user + display_options: null + field: user_updated + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar}} {{first_name}} {{last_name}}' + readonly: true + required: false + searchable: true + sort: 15 + special: + - user-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_updated + table: user_rigs + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_rigs + field: date_updated + type: timestamp + meta: + collection: user_rigs + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 16 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: user_rigs + data_type: timestamp + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: owner + type: string + meta: + collection: user_rigs + conditions: null + display: related-values + display_options: + template: '{{username}}' + field: owner + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: owner + table: user_rigs + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: user_rigs + field: name + type: string + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: name + table: user_rigs + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: user_rigs + field: rig_type + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: rig_type + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: rig_type + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: user_rig_type + foreign_key_column: id + - collection: user_rigs + field: laser_source + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_source + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: null + readonly: false + required: true + searchable: true + sort: 5 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_source + table: user_rigs + data_type: int + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_source + foreign_key_column: submission_id + - collection: user_rigs + field: laser_scan_lens + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_scan_lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{field_size}} {{focal_length}}' + readonly: false + required: false + searchable: true + sort: 7 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_scan_lens + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens + foreign_key_column: id + - collection: user_rigs + field: laser_focus_lens + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_focus_lens + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 6 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_focus_lens + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_focus_lens + foreign_key_column: id + - collection: user_rigs + field: laser_scan_lens_apt + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_scan_lens_apt + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 8 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_scan_lens_apt + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens_apt + foreign_key_column: id + - collection: user_rigs + field: laser_scan_lens_exp + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_scan_lens_exp + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_scan_lens_exp + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_scan_lens_exp + foreign_key_column: id + - collection: user_rigs + field: laser_software + type: integer + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: laser_software + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{name}}' + readonly: false + required: false + searchable: true + sort: 10 + special: + - m2o + translations: null + validation: null + validation_message: null + width: full + schema: + name: laser_software + table: user_rigs + data_type: int unsigned + default_value: null + max_length: null + numeric_precision: 10 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: laser_software + foreign_key_column: id + - collection: user_rigs + field: notes + type: text + meta: + collection: user_rigs + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: null + options: null + readonly: false + required: false + searchable: true + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: user_rigs + data_type: text + default_value: null + max_length: 65535 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null +systemFields: + - collection: directus_access + field: policy + schema: + is_indexed: true + - collection: directus_access + field: role + schema: + is_indexed: true + - collection: directus_access + field: user + schema: + is_indexed: true + - collection: directus_activity + field: collection + schema: + is_indexed: true + - collection: directus_activity + field: timestamp + schema: + is_indexed: true + - collection: directus_collections + field: group + schema: + is_indexed: true + - collection: directus_comments + field: user_created + schema: + is_indexed: true + - collection: directus_comments + field: user_updated + schema: + is_indexed: true + - collection: directus_dashboards + field: user_created + schema: + is_indexed: true + - collection: directus_deployment_projects + field: user_created + schema: + is_indexed: true + - collection: directus_deployment_runs + field: project + schema: + is_indexed: true + - collection: directus_deployment_runs + field: user_created + schema: + is_indexed: true + - collection: directus_deployments + field: user_created + schema: + is_indexed: true + - collection: directus_fields + field: collection + schema: + is_indexed: true + - collection: directus_files + field: folder + schema: + is_indexed: true + - collection: directus_files + field: modified_by + schema: + is_indexed: true + - collection: directus_files + field: uploaded_by + schema: + is_indexed: true + - collection: directus_flows + field: user_created + schema: + is_indexed: true + - collection: directus_folders + field: parent + schema: + is_indexed: true + - collection: directus_notifications + field: recipient + schema: + is_indexed: true + - collection: directus_notifications + field: sender + schema: + is_indexed: true + - collection: directus_oauth_clients + field: date_created + schema: + is_indexed: true + - collection: directus_oauth_codes + field: client + schema: + is_indexed: true + - collection: directus_oauth_codes + field: expires_at + schema: + is_indexed: true + - collection: directus_oauth_codes + field: used_at + schema: + is_indexed: true + - collection: directus_oauth_codes + field: user + schema: + is_indexed: true + - collection: directus_oauth_consents + field: client + schema: + is_indexed: true + - collection: directus_oauth_tokens + field: code_hash + schema: + is_indexed: true + - collection: directus_oauth_tokens + field: expires_at + schema: + is_indexed: true + - collection: directus_oauth_tokens + field: previous_session + schema: + is_indexed: true + - collection: directus_oauth_tokens + field: session + schema: + is_indexed: true + - collection: directus_oauth_tokens + field: user + schema: + is_indexed: true + - collection: directus_operations + field: flow + schema: + is_indexed: true + - collection: directus_operations + field: user_created + schema: + is_indexed: true + - collection: directus_panels + field: dashboard + schema: + is_indexed: true + - collection: directus_panels + field: user_created + schema: + is_indexed: true + - collection: directus_permissions + field: collection + schema: + is_indexed: true + - collection: directus_permissions + field: policy + schema: + is_indexed: true + - collection: directus_presets + field: collection + schema: + is_indexed: true + - collection: directus_presets + field: role + schema: + is_indexed: true + - collection: directus_presets + field: user + schema: + is_indexed: true + - collection: directus_relations + field: many_collection + schema: + is_indexed: true + - collection: directus_relations + field: one_collection + schema: + is_indexed: true + - collection: directus_revisions + field: activity + schema: + is_indexed: true + - collection: directus_revisions + field: collection + schema: + is_indexed: true + - collection: directus_revisions + field: parent + schema: + is_indexed: true + - collection: directus_revisions + field: version + schema: + is_indexed: true + - collection: directus_roles + field: parent + schema: + is_indexed: true + - collection: directus_sessions + field: share + schema: + is_indexed: true + - collection: directus_sessions + field: user + schema: + is_indexed: true + - collection: directus_settings + field: project_logo + schema: + is_indexed: true + - collection: directus_settings + field: public_background + schema: + is_indexed: true + - collection: directus_settings + field: public_favicon + schema: + is_indexed: true + - collection: directus_settings + field: public_foreground + schema: + is_indexed: true + - collection: directus_settings + field: public_registration_role + schema: + is_indexed: true + - collection: directus_settings + field: storage_default_folder + schema: + is_indexed: true + - collection: directus_shares + field: collection + schema: + is_indexed: true + - collection: directus_shares + field: role + schema: + is_indexed: true + - collection: directus_shares + field: user_created + schema: + is_indexed: true + - collection: directus_users + field: role + schema: + is_indexed: true + - collection: directus_versions + field: collection + schema: + is_indexed: true + - collection: directus_versions + field: user_created + schema: + is_indexed: true + - collection: directus_versions + field: user_updated + schema: + is_indexed: true +relations: + - collection: att_color + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: att_color + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_color + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: att_color + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_p_type + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: att_p_type + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_p_type + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: att_p_type + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_scan_lens + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: att_scan_lens + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_scan_lens + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: att_scan_lens + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_software + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: att_software + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: att_software + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: att_software + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: bg_cat + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_cat + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_cat + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_cat_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_cat + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_cat + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_cat + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_cat_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_entries + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_entries + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_entries_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_entries + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_entries + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_entries_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_entries + field: index + related_collection: directus_files + meta: + junction_field: null + many_collection: bg_entries + many_field: index + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: index + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: bg_entries_index_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: bg_entries + field: header + related_collection: directus_files + meta: + junction_field: null + many_collection: bg_entries + many_field: header + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: header + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: bg_entries_header_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: bg_entries + field: bg_entry_sub_cat + related_collection: bg_sub_cat + meta: + junction_field: null + many_collection: bg_entries + many_field: bg_entry_sub_cat + one_allowed_collections: null + one_collection: bg_sub_cat + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: bg_entry_sub_cat + foreign_key_table: bg_sub_cat + foreign_key_column: id + constraint_name: bg_entries_bg_entry_sub_cat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: bg_entries + field: bg_entry_cat + related_collection: bg_cat + meta: + junction_field: null + many_collection: bg_entries + many_field: bg_entry_cat + one_allowed_collections: null + one_collection: bg_cat + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_entries + column: bg_entry_cat + foreign_key_table: bg_cat + foreign_key_column: id + constraint_name: bg_entries_bg_entry_cat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: bg_sub_cat + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_sub_cat + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_sub_cat + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_sub_cat_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_sub_cat + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: bg_sub_cat + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_sub_cat + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: bg_sub_cat_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: bg_sub_cat + field: bg_entry_cat + related_collection: bg_cat + meta: + junction_field: null + many_collection: bg_sub_cat + many_field: bg_entry_cat + one_allowed_collections: null + one_collection: bg_cat + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: bg_sub_cat + column: bg_entry_cat + foreign_key_table: bg_cat + foreign_key_column: id + constraint_name: bg_sub_cat_bg_entry_cat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: hazard_danger + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_danger + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_danger + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_danger_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_danger + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_danger + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_danger + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_danger_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_severity + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_severity + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_severity + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_severity_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_severity + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_severity + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_severity + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_severity_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_source + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_source + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_source + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_source_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_source + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_source + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_source + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_source_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_tags + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_tags + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_tags + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_tags_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_tags + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: hazard_tags + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_tags + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: hazard_tags_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: hazard_tags + field: hazard_source + related_collection: hazard_source + meta: + junction_field: null + many_collection: hazard_tags + many_field: hazard_source + one_allowed_collections: null + one_collection: hazard_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_tags + column: hazard_source + foreign_key_table: hazard_source + foreign_key_column: id + constraint_name: hazard_tags_hazard_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: hazard_tags + field: hazard_danger + related_collection: hazard_danger + meta: + junction_field: null + many_collection: hazard_tags + many_field: hazard_danger + one_allowed_collections: null + one_collection: hazard_danger + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_tags + column: hazard_danger + foreign_key_table: hazard_danger + foreign_key_column: id + constraint_name: hazard_tags_hazard_danger_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: hazard_tags + field: hazard_severity + related_collection: hazard_severity + meta: + junction_field: null + many_collection: hazard_tags + many_field: hazard_severity + one_allowed_collections: null + one_collection: hazard_severity + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: hazard_tags + column: hazard_severity + foreign_key_table: hazard_severity + foreign_key_column: id + constraint_name: hazard_tags_hazard_severity_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: laser_focus_lens + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_focus_lens + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_focus_lens_config + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens_config + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens_config + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_config_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_focus_lens_config + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens_config + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens_config + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_config_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_focus_lens_diameter + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens_diameter + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens_diameter + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_diameter_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_focus_lens_diameter + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_focus_lens_diameter + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_focus_lens_diameter + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_focus_lens_diameter_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_apt + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_apt + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_apt + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_apt_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_apt + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_apt + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_apt + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_apt_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_config + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_config + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_config + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_config_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_config + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_config + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_config + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_config_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_exp + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_exp + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_exp + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_exp_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: laser_scan_lens_exp + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: laser_scan_lens_exp + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: laser_scan_lens_exp + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: laser_scan_lens_exp_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: le_fiber_settings + field: material_db + related_collection: le_materials + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: material_db + one_allowed_collections: null + one_collection: le_materials + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: color + related_collection: att_color + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: color + one_allowed_collections: null + one_collection: att_color + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: p_type + related_collection: att_p_type + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: p_type + one_allowed_collections: null + one_collection: att_p_type + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: fiber_settings_photos + related_collection: directus_files + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: fiber_settings_photos + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: fiber_settings_screenshots + related_collection: directus_files + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: fiber_settings_screenshots + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: software_type + related_collection: att_software + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: software_type + one_allowed_collections: null + one_collection: att_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_fiber_settings + field: scan_lens + related_collection: att_scan_lens + meta: + junction_field: null + many_collection: le_fiber_settings + many_field: scan_lens + one_allowed_collections: null + one_collection: att_scan_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_material_categories + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: le_material_categories + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_material_categories + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: le_material_categories + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: le_materials + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: le_materials + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials + field: material_category + related_collection: le_material_categories + meta: + junction_field: null + many_collection: le_materials + many_field: material_category + one_allowed_collections: null + one_collection: le_material_categories + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials + field: safety_status + related_collection: le_safety_status + meta: + junction_field: null + many_collection: le_materials + many_field: safety_status + one_allowed_collections: null + one_collection: le_safety_status + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials_le_safety_tags + field: le_safety_tags_id + related_collection: le_safety_tags + meta: + junction_field: le_materials_id + many_collection: le_materials_le_safety_tags + many_field: le_safety_tags_id + one_allowed_collections: null + one_collection: le_safety_tags + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_materials_le_safety_tags + field: le_materials_id + related_collection: le_materials + meta: + junction_field: le_safety_tags_id + many_collection: le_materials_le_safety_tags + many_field: le_materials_id + one_allowed_collections: null + one_collection: le_materials + one_collection_field: null + one_deselect_action: nullify + one_field: safety_tags + sort_field: null + - collection: le_safety_status + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: le_safety_status + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_safety_status + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: le_safety_status + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_safety_tags + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: le_safety_tags + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: le_safety_tags + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: le_safety_tags + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + - collection: material + field: material_cat + related_collection: material_cat + meta: + junction_field: null + many_collection: material + many_field: material_cat + one_allowed_collections: null + one_collection: material_cat + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material + column: material_cat + foreign_key_table: material_cat + foreign_key_column: id + constraint_name: material_material_cat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material + field: material_status + related_collection: material_status + meta: + junction_field: null + many_collection: material + many_field: material_status + one_allowed_collections: null + one_collection: material_status + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material + column: material_status + foreign_key_table: material_status + foreign_key_column: id + constraint_name: material_material_status_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_coating + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: material_coating + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_coating + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: material_coating_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: material_coating + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: material_coating + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_coating + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: material_coating_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: material_coating + field: coating_status + related_collection: material_status + meta: + junction_field: null + many_collection: material_coating + many_field: coating_status + one_allowed_collections: null + one_collection: material_status + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_coating + column: coating_status + foreign_key_table: material_status + foreign_key_column: id + constraint_name: material_coating_coating_status_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_coating_hazard_tags + field: hazard_tags_id + related_collection: hazard_tags + meta: + junction_field: material_coating_id + many_collection: material_coating_hazard_tags + many_field: hazard_tags_id + one_allowed_collections: null + one_collection: hazard_tags + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_coating_hazard_tags + column: hazard_tags_id + foreign_key_table: hazard_tags + foreign_key_column: id + constraint_name: material_coating_hazard_tags_hazard_tags_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_coating_hazard_tags + field: material_coating_id + related_collection: material_coating + meta: + junction_field: hazard_tags_id + many_collection: material_coating_hazard_tags + many_field: material_coating_id + one_allowed_collections: null + one_collection: material_coating + one_collection_field: null + one_deselect_action: nullify + one_field: hazard_tags + sort_field: null + schema: + table: material_coating_hazard_tags + column: material_coating_id + foreign_key_table: material_coating + foreign_key_column: id + constraint_name: material_coating_hazard_tags_material_coating_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_hazard_tags + field: hazard_tags_id + related_collection: hazard_tags + meta: + junction_field: material_id + many_collection: material_hazard_tags + many_field: hazard_tags_id + one_allowed_collections: null + one_collection: hazard_tags + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_hazard_tags + column: hazard_tags_id + foreign_key_table: hazard_tags + foreign_key_column: id + constraint_name: material_hazard_tags_hazard_tags_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_hazard_tags + field: material_id + related_collection: material + meta: + junction_field: hazard_tags_id + many_collection: material_hazard_tags + many_field: material_id + one_allowed_collections: null + one_collection: material + one_collection_field: null + one_deselect_action: nullify + one_field: hazard_tags + sort_field: null + schema: + table: material_hazard_tags + column: material_id + foreign_key_table: material + foreign_key_column: id + constraint_name: material_hazard_tags_material_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: material_opacity + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: material_opacity + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_opacity + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: material_opacity_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: material_opacity + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: material_opacity + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: material_opacity + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: material_opacity_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: projects + field: p_image + related_collection: directus_files + meta: + junction_field: null + many_collection: projects + many_field: p_image + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: projects + column: p_image + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: projects_p_image_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: projects + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: projects + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: projects + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: projects_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: projects_files + field: directus_files_id + related_collection: directus_files + meta: + junction_field: projects_submission_id + many_collection: projects_files + many_field: directus_files_id + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: projects_files + column: directus_files_id + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: projects_files_directus_files_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: projects_files + field: projects_submission_id + related_collection: projects + meta: + junction_field: directus_files_id + many_collection: projects_files + many_field: projects_submission_id + one_allowed_collections: null + one_collection: projects + one_collection_field: null + one_deselect_action: nullify + one_field: p_files + sort_field: null + schema: + table: projects_files + column: projects_submission_id + foreign_key_table: projects + foreign_key_column: submission_id + constraint_name: projects_files_projects_submission_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: photo + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_co2gal + many_field: photo + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: photo + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_co2gal_photo_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: screen + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_co2gal + many_field: screen + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: screen + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_co2gal_screen_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: source + related_collection: laser_source + meta: + junction_field: null + many_collection: settings_co2gal + many_field: source + one_allowed_collections: null + one_collection: laser_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: source + foreign_key_table: laser_source + foreign_key_column: submission_id + constraint_name: settings_co2gal_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: lens + related_collection: laser_scan_lens + meta: + junction_field: settings_co2gal_submission_id + many_collection: settings_co2gal + many_field: lens + one_allowed_collections: [] + one_collection: laser_scan_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: lens + foreign_key_table: laser_scan_lens + foreign_key_column: id + constraint_name: settings_co2gal_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: mat + related_collection: material + meta: + junction_field: null + many_collection: settings_co2gal + many_field: mat + one_allowed_collections: null + one_collection: material + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: mat + foreign_key_table: material + foreign_key_column: id + constraint_name: settings_co2gal_mat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: mat_coat + related_collection: material_coating + meta: + junction_field: null + many_collection: settings_co2gal + many_field: mat_coat + one_allowed_collections: null + one_collection: material_coating + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: mat_coat + foreign_key_table: material_coating + foreign_key_column: id + constraint_name: settings_co2gal_mat_coat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: mat_color + related_collection: material_color + meta: + junction_field: null + many_collection: settings_co2gal + many_field: mat_color + one_allowed_collections: null + one_collection: material_color + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: mat_color + foreign_key_table: material_color + foreign_key_column: id + constraint_name: settings_co2gal_mat_color_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: mat_opacity + related_collection: material_opacity + meta: + junction_field: null + many_collection: settings_co2gal + many_field: mat_opacity + one_allowed_collections: null + one_collection: material_opacity + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: mat_opacity + foreign_key_table: material_opacity + foreign_key_column: id + constraint_name: settings_co2gal_mat_opacity_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: laser_soft + related_collection: laser_software + meta: + junction_field: null + many_collection: settings_co2gal + many_field: laser_soft + one_allowed_collections: null + one_collection: laser_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: laser_soft + foreign_key_table: laser_software + foreign_key_column: id + constraint_name: settings_co2gal_laser_soft_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: lens_conf + related_collection: laser_scan_lens_config + meta: + junction_field: null + many_collection: settings_co2gal + many_field: lens_conf + one_allowed_collections: null + one_collection: laser_scan_lens_config + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: lens_conf + foreign_key_table: laser_scan_lens_config + foreign_key_column: id + constraint_name: settings_co2gal_lens_conf_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: lens_apt + related_collection: laser_scan_lens_apt + meta: + junction_field: null + many_collection: settings_co2gal + many_field: lens_apt + one_allowed_collections: null + one_collection: laser_scan_lens_apt + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: lens_apt + foreign_key_table: laser_scan_lens_apt + foreign_key_column: id + constraint_name: settings_co2gal_lens_apt_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: lens_exp + related_collection: laser_scan_lens_exp + meta: + junction_field: null + many_collection: settings_co2gal + many_field: lens_exp + one_allowed_collections: null + one_collection: laser_scan_lens_exp + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: lens_exp + foreign_key_table: laser_scan_lens_exp + foreign_key_column: id + constraint_name: settings_co2gal_lens_exp_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gal + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: settings_co2gal + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gal + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: settings_co2gal_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: screen + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_co2gan + many_field: screen + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: screen + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_co2gan_screen_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: source + related_collection: laser_source + meta: + junction_field: null + many_collection: settings_co2gan + many_field: source + one_allowed_collections: null + one_collection: laser_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: source + foreign_key_table: laser_source + foreign_key_column: submission_id + constraint_name: settings_co2gan_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: mat + related_collection: material + meta: + junction_field: null + many_collection: settings_co2gan + many_field: mat + one_allowed_collections: null + one_collection: material + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: mat + foreign_key_table: material + foreign_key_column: id + constraint_name: settings_co2gan_mat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: mat_coat + related_collection: material_coating + meta: + junction_field: null + many_collection: settings_co2gan + many_field: mat_coat + one_allowed_collections: null + one_collection: material_coating + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: mat_coat + foreign_key_table: material_coating + foreign_key_column: id + constraint_name: settings_co2gan_mat_coat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: mat_color + related_collection: material_color + meta: + junction_field: null + many_collection: settings_co2gan + many_field: mat_color + one_allowed_collections: null + one_collection: material_color + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: mat_color + foreign_key_table: material_color + foreign_key_column: id + constraint_name: settings_co2gan_mat_color_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: mat_opacity + related_collection: material_opacity + meta: + junction_field: null + many_collection: settings_co2gan + many_field: mat_opacity + one_allowed_collections: null + one_collection: material_opacity + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: mat_opacity + foreign_key_table: material_opacity + foreign_key_column: id + constraint_name: settings_co2gan_mat_opacity_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: laser_soft + related_collection: laser_software + meta: + junction_field: null + many_collection: settings_co2gan + many_field: laser_soft + one_allowed_collections: null + one_collection: laser_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: laser_soft + foreign_key_table: laser_software + foreign_key_column: id + constraint_name: settings_co2gan_laser_soft_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: photo + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_co2gan + many_field: photo + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: photo + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_co2gan_photo_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: lens + related_collection: laser_focus_lens + meta: + junction_field: null + many_collection: settings_co2gan + many_field: lens + one_allowed_collections: null + one_collection: laser_focus_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: lens + foreign_key_table: laser_focus_lens + foreign_key_column: id + constraint_name: settings_co2gan_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: lens_conf + related_collection: laser_focus_lens_config + meta: + junction_field: null + many_collection: settings_co2gan + many_field: lens_conf + one_allowed_collections: null + one_collection: laser_focus_lens_config + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: lens_conf + foreign_key_table: laser_focus_lens_config + foreign_key_column: id + constraint_name: settings_co2gan_lens_conf_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_co2gan + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: settings_co2gan + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_co2gan + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: settings_co2gan_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: photo + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_fiber + many_field: photo + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: photo + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_fiber_photo_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: screen + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_fiber + many_field: screen + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: screen + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_fiber_screen_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: source + related_collection: laser_source + meta: + junction_field: null + many_collection: settings_fiber + many_field: source + one_allowed_collections: null + one_collection: laser_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: source + foreign_key_table: laser_source + foreign_key_column: submission_id + constraint_name: settings_fiber_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: lens + related_collection: laser_scan_lens + meta: + junction_field: null + many_collection: settings_fiber + many_field: lens + one_allowed_collections: null + one_collection: laser_scan_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: lens + foreign_key_table: laser_scan_lens + foreign_key_column: id + constraint_name: settings_fiber_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: mat + related_collection: material + meta: + junction_field: null + many_collection: settings_fiber + many_field: mat + one_allowed_collections: null + one_collection: material + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: mat + foreign_key_table: material + foreign_key_column: id + constraint_name: settings_fiber_mat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: mat_color + related_collection: material_color + meta: + junction_field: null + many_collection: settings_fiber + many_field: mat_color + one_allowed_collections: null + one_collection: material_color + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: mat_color + foreign_key_table: material_color + foreign_key_column: id + constraint_name: settings_fiber_mat_color_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: laser_soft + related_collection: laser_software + meta: + junction_field: null + many_collection: settings_fiber + many_field: laser_soft + one_allowed_collections: null + one_collection: laser_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: laser_soft + foreign_key_table: laser_software + foreign_key_column: id + constraint_name: settings_fiber_laser_soft_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: mat_coat + related_collection: material_coating + meta: + junction_field: null + many_collection: settings_fiber + many_field: mat_coat + one_allowed_collections: null + one_collection: material_coating + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: mat_coat + foreign_key_table: material_coating + foreign_key_column: id + constraint_name: settings_fiber_mat_coat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: mat_opacity + related_collection: material_opacity + meta: + junction_field: null + many_collection: settings_fiber + many_field: mat_opacity + one_allowed_collections: null + one_collection: material_opacity + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: mat_opacity + foreign_key_table: material_opacity + foreign_key_column: id + constraint_name: settings_fiber_mat_opacity_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_fiber + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: settings_fiber + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_fiber + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: settings_fiber_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: screen + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_uv + many_field: screen + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: screen + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_uv_screen_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: photo + related_collection: directus_files + meta: + junction_field: null + many_collection: settings_uv + many_field: photo + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: photo + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: settings_uv_photo_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: source + related_collection: laser_source + meta: + junction_field: null + many_collection: settings_uv + many_field: source + one_allowed_collections: null + one_collection: laser_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: source + foreign_key_table: laser_source + foreign_key_column: submission_id + constraint_name: settings_uv_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: lens + related_collection: laser_scan_lens + meta: + junction_field: null + many_collection: settings_uv + many_field: lens + one_allowed_collections: null + one_collection: laser_scan_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: lens + foreign_key_table: laser_scan_lens + foreign_key_column: id + constraint_name: settings_uv_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: mat + related_collection: material + meta: + junction_field: null + many_collection: settings_uv + many_field: mat + one_allowed_collections: null + one_collection: material + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: mat + foreign_key_table: material + foreign_key_column: id + constraint_name: settings_uv_mat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: mat_coat + related_collection: material_coating + meta: + junction_field: null + many_collection: settings_uv + many_field: mat_coat + one_allowed_collections: null + one_collection: material_coating + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: mat_coat + foreign_key_table: material_coating + foreign_key_column: id + constraint_name: settings_uv_mat_coat_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: mat_color + related_collection: material_color + meta: + junction_field: null + many_collection: settings_uv + many_field: mat_color + one_allowed_collections: null + one_collection: material_color + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: mat_color + foreign_key_table: material_color + foreign_key_column: id + constraint_name: settings_uv_mat_color_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: mat_opacity + related_collection: material_opacity + meta: + junction_field: null + many_collection: settings_uv + many_field: mat_opacity + one_allowed_collections: null + one_collection: material_opacity + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: mat_opacity + foreign_key_table: material_opacity + foreign_key_column: id + constraint_name: settings_uv_mat_opacity_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: settings_uv + field: laser_soft + related_collection: laser_software + meta: + junction_field: null + many_collection: settings_uv + many_field: laser_soft + one_allowed_collections: null + one_collection: laser_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: laser_soft + foreign_key_table: laser_software + foreign_key_column: id + constraint_name: settings_uv_laser_soft_foreign + on_update: RESTRICT + on_delete: NO ACTION + - collection: settings_uv + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: settings_uv + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: settings_uv + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: settings_uv_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_claims + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: user_claims + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_claims + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_claims_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_claims + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: user_claims + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_claims + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_claims_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_claims + field: claimant + related_collection: directus_users + meta: + junction_field: null + many_collection: user_claims + many_field: claimant + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_claims + column: claimant + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_claims_claimant_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_claims + field: reviewed_by + related_collection: directus_users + meta: + junction_field: null + many_collection: user_claims + many_field: reviewed_by + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_claims + column: reviewed_by + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_claims_reviewed_by_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_memberships + field: app_user + related_collection: directus_users + meta: + junction_field: null + many_collection: user_memberships + many_field: app_user + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_memberships + column: app_user + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_memberships_app_user_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_memberships + field: claim_user_id + related_collection: directus_users + meta: + junction_field: null + many_collection: user_memberships + many_field: claim_user_id + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_memberships + column: claim_user_id + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_memberships_claim_user_id_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_preferences + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: user_preferences + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_preferences + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_preferences_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_preferences + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: user_preferences + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_preferences + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_preferences_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_rigs + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: user_rigs + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_rigs_user_created_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_rigs + field: user_updated + related_collection: directus_users + meta: + junction_field: null + many_collection: user_rigs + many_field: user_updated + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: user_updated + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_rigs_user_updated_foreign + on_update: RESTRICT + on_delete: RESTRICT + - collection: user_rigs + field: owner + related_collection: directus_users + meta: + junction_field: null + many_collection: user_rigs + many_field: owner + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: owner + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: user_rigs_owner_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: rig_type + related_collection: user_rig_type + meta: + junction_field: null + many_collection: user_rigs + many_field: rig_type + one_allowed_collections: null + one_collection: user_rig_type + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: rig_type + foreign_key_table: user_rig_type + foreign_key_column: id + constraint_name: user_rigs_rig_type_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_source + related_collection: laser_source + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_source + one_allowed_collections: null + one_collection: laser_source + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_source + foreign_key_table: laser_source + foreign_key_column: submission_id + constraint_name: user_rigs_laser_source_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_scan_lens + related_collection: laser_scan_lens + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_scan_lens + one_allowed_collections: null + one_collection: laser_scan_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_scan_lens + foreign_key_table: laser_scan_lens + foreign_key_column: id + constraint_name: user_rigs_laser_scan_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_focus_lens + related_collection: laser_focus_lens + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_focus_lens + one_allowed_collections: null + one_collection: laser_focus_lens + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_focus_lens + foreign_key_table: laser_focus_lens + foreign_key_column: id + constraint_name: user_rigs_laser_focus_lens_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_scan_lens_apt + related_collection: laser_scan_lens_apt + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_scan_lens_apt + one_allowed_collections: null + one_collection: laser_scan_lens_apt + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_scan_lens_apt + foreign_key_table: laser_scan_lens_apt + foreign_key_column: id + constraint_name: user_rigs_laser_scan_lens_apt_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_scan_lens_exp + related_collection: laser_scan_lens_exp + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_scan_lens_exp + one_allowed_collections: null + one_collection: laser_scan_lens_exp + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_scan_lens_exp + foreign_key_table: laser_scan_lens_exp + foreign_key_column: id + constraint_name: user_rigs_laser_scan_lens_exp_foreign + on_update: RESTRICT + on_delete: SET NULL + - collection: user_rigs + field: laser_software + related_collection: laser_software + meta: + junction_field: null + many_collection: user_rigs + many_field: laser_software + one_allowed_collections: null + one_collection: laser_software + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: user_rigs + column: laser_software + foreign_key_table: laser_software + foreign_key_column: id + constraint_name: user_rigs_laser_software_foreign + on_update: RESTRICT + on_delete: SET NULL From 4884b5d3672673672c8b698b72b6ffafef12eb23 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 10 Jul 2026 11:13:18 -0400 Subject: [PATCH 02/14] Retry authorized Directus registration credentials --- app/lib/directus.ts | 57 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/app/lib/directus.ts b/app/lib/directus.ts index 950e23c..d17bdc1 100644 --- a/app/lib/directus.ts +++ b/app/lib/directus.ts @@ -10,18 +10,21 @@ const BASE = ( "" ).replace(/\/$/, ""); // DIRECTUS_TOKEN_ADMIN_REGISTER is the canonical credential for the production -// Registration Bot policy. Keep the older aliases as fallbacks so an existing -// deployment does not silently lose authentication during migration. -const TOKEN_ADMIN_REGISTER = - process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || - process.env.DIRECTUS_SERVICE_TOKEN || - process.env.DIRECTUS_ADMIN_TOKEN || - ""; // server-only +// Registration Bot policy. Keep older aliases as independently tested fallbacks: +// a configured but stale token must not hide a later valid credential. +const REGISTRATION_CREDENTIALS = [ + ["DIRECTUS_TOKEN_ADMIN_REGISTER", process.env.DIRECTUS_TOKEN_ADMIN_REGISTER], + ["DIRECTUS_SERVICE_TOKEN", process.env.DIRECTUS_SERVICE_TOKEN], + ["DIRECTUS_ADMIN_TOKEN", process.env.DIRECTUS_ADMIN_TOKEN], +].filter( + (entry, index, entries): entry is [string, string] => + Boolean(entry[1]) && entries.findIndex((other) => other[1] === entry[1]) === index +); const ROLE_MEMBER_NAME = process.env.DIRECTUS_ROLE_MEMBER_NAME || "Users"; const PROJECTS_COLLECTION = process.env.DIRECTUS_PROJECTS_COLLECTION || "projects"; if (!BASE) console.warn("[directus] Missing DIRECTUS_URL / NEXT_PUBLIC_API_BASE_URL"); -if (!TOKEN_ADMIN_REGISTER) +if (!REGISTRATION_CREDENTIALS.length) console.warn( "[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (or legacy service-token alias) " + "used for registration and username login" @@ -134,17 +137,33 @@ export async function dxDELETE(path: string, bearer: string): Promise(path: string, init?: RequestInit): Promise { if (!BASE) throw new Error("Missing Directus base URL"); - if (!TOKEN_ADMIN_REGISTER) throw new Error("Missing Directus registration credential"); - const res = await fetch(`${BASE}${path}`, { - ...init, - headers: { - Accept: "application/json", - Authorization: `Bearer ${TOKEN_ADMIN_REGISTER}`, - ...(init?.headers || {}), - }, - cache: "no-store", - }); - return (await throwIfNotOk(res)) as T; + if (!REGISTRATION_CREDENTIALS.length) throw new Error("Missing Directus registration credential"); + + let lastAuthorizationError: any = null; + for (const [credentialName, credential] of REGISTRATION_CREDENTIALS) { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + Accept: "application/json", + Authorization: `Bearer ${credential}`, + ...(init?.headers || {}), + }, + cache: "no-store", + }); + + try { + return (await throwIfNotOk(res)) as T; + } catch (error: any) { + if (error?.status !== 401 && error?.status !== 403) throw error; + lastAuthorizationError = error; + console.warn(`[directus] Registration credential rejected: ${credentialName}`, { + status: error.status, + code: error?.detail?.errors?.[0]?.extensions?.code, + }); + } + } + + throw lastAuthorizationError ?? new Error("No authorized Directus registration credential"); } // ───────────────────────────────────────────────────────────── From ed1c84eb76d21001213ec3d0f0eb0adc290c74fc Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 10 Jul 2026 20:02:57 -0400 Subject: [PATCH 03/14] document payload migration runtime plan --- .dockerignore | 16 + .gitignore | 7 + .node-version | 1 + .nvmrc | 1 + app/.dockerignore | 11 + bgbye/.dockerignore | 21 ++ .../architecture-proposal.md | 146 +++++++++ docs/payload-migration/environment-parity.md | 274 +++++++++++++++++ docs/payload-migration/host-runtime-plan.md | 291 ++++++++++++++++++ docs/payload-migration/implementation-plan.md | 82 +++++ .../payload-migration/metadata-disposition.md | 28 ++ payload/.dockerignore | 14 + 12 files changed, 892 insertions(+) create mode 100644 .dockerignore create mode 100644 .node-version create mode 100644 .nvmrc create mode 100644 bgbye/.dockerignore create mode 100644 docs/payload-migration/architecture-proposal.md create mode 100644 docs/payload-migration/environment-parity.md create mode 100644 docs/payload-migration/host-runtime-plan.md create mode 100644 docs/payload-migration/implementation-plan.md create mode 100644 docs/payload-migration/metadata-disposition.md create mode 100644 payload/.dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..570a252 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.worktrees +.migration-source +.migration.env +**/*.sql +**/*.sql.gz +**/node_modules +**/.next +payload/migration/generated +**/uploads +**/media +**/migration/generated +**/test-results +**/playwright-report +**/blob-report +**/.auth diff --git a/.gitignore b/.gitignore index 28ab8db..5ec752e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,10 @@ app/components/account.zip # Accidental root lockfile; applications have their own lockfiles /package-lock.json + +# Local migration inputs and isolated worktrees +.worktrees/ +.migration-source +.migration-source/ +.migration.env +payload/migration/generated/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.18.0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/app/.dockerignore b/app/.dockerignore index 6c349af..5ade1b1 100644 --- a/app/.dockerignore +++ b/app/.dockerignore @@ -17,3 +17,14 @@ debug *.bak *.zip .DS_Store + +.migration-source +.migration.env +.worktrees +*.sql +*.sql.gz +uploads +media +migration/generated +test-results +playwright-report diff --git a/bgbye/.dockerignore b/bgbye/.dockerignore new file mode 100644 index 0000000..a7b0a1e --- /dev/null +++ b/bgbye/.dockerignore @@ -0,0 +1,21 @@ +.git +.gitignore +.worktrees +.migration-source +.migration.env +node_modules +.next +.cache +.u2net +.ormbg +.models +temp_videos +uploads +media +migration +test-results +playwright-report +*.sql +*.sql.gz +*.log +*.bak diff --git a/docs/payload-migration/architecture-proposal.md b/docs/payload-migration/architecture-proposal.md new file mode 100644 index 0000000..bd68017 --- /dev/null +++ b/docs/payload-migration/architecture-proposal.md @@ -0,0 +1,146 @@ +# Payload migration architecture proposal + +Status: pre-implementation decision record. No production changes are authorized by this document. + +## Objective and Milestone 0 + +The target is a unified, reproducible application environment, not merely a database conversion. Unified means one repository, one versioned orchestration model, shared service contracts, consistent application images, and documented promotion from development through production. Services remain separate processes and containers. + +**Milestone 0: complete reproducible application environment** must be accepted and implemented before the large Payload/data-migration phase. It delivers: + +- complete service and persistence inventory for the `4884b5d` application; +- common Compose core plus mode-specific overlays/profiles; +- rootless, Codex-controlled nonproduction containers and volumes; +- production-like local HTTPS routing and cookie behavior; +- fixture and scrubbed-snapshot data modes using identical application images; +- safe captured email and outbound-network safeguards; +- optional isolated legacy Directus reference environment; +- deterministic one-command lifecycle, migration, validation, smoke, and browser-test workflows; +- image-by-digest promotion to staging and production. + +The detailed runtime inventory, orchestration design, repository tree, and test matrix are in [`environment-parity.md`](./environment-parity.md). The access audit, recommended account boundary, exact host prerequisites, ingress integration, storage, networking proof, and rollback plan are in [`host-runtime-plan.md`](./host-runtime-plan.md). The gated delivery sequence is in [`implementation-plan.md`](./implementation-plan.md). + +Milestone 0 host rule: nonproduction container isolation is not treated as a substitute for host filesystem permissions. The chosen daemon owner must be unable to read production paths and secrets, because a container controller can bind-mount anything readable to that daemon account. + +## Verified baseline + +The migration branch initially started from `main` at `0a7ee5f`. A tree and history comparison against the final pre-utilities production commit `4884b5d` established: + +- `main` is an ancestor of `4884b5d`. +- `4884b5d` contains exactly two later commits: `92ce8bc` and `4884b5d`. +- Those commits repair production registration/authentication behavior and add the verified Directus deployment contract, policies, flows, exports, and schema snapshot. +- The application was not replaced or reduced in those commits. + +Conclusion: `main` is a complete application tree, but `4884b5d` is the correct and more complete pre-utilities production baseline. The dedicated `migration/payload` branch has therefore been fast-forwarded to `4884b5d`. The temporary utilities branch is not an ancestor of the migration work. + +## Service boundary + +The default architecture is three independent services: + +```text +browser + | + v +existing Next.js application (frontend + compatibility API) + | | + | server-to-server HTTP | background-removal calls + v v +dedicated Payload service existing bgbye service + | + v +dedicated PostgreSQL database +``` + +Payload owns content administration, authentication records, media metadata, access policy, hooks, and its generated APIs. The existing application remains the public frontend and initially retains its public API routes. Those routes become a compatibility layer backed by Payload, permitting endpoint-by-endpoint cutover and response-shape comparison. + +Deeply embedding Payload into the existing frontend would reduce one network boundary and allow direct Local API calls. It would also force the frontend onto Payload's exact Next.js dependency range, combine release and failure domains, increase the blast radius of admin changes, and make rollback harder. That integration is deferred pending explicit approval. + +The complete topology adds a reverse proxy, PostgreSQL, captured mail, migration/test jobs, and optional local-only Directus reference services around this accepted boundary. Payload remains independently deployable and is never merged into the frontend process by Milestone 0. + +## Runtime and dependency matrix + +| Component | Pinned version | Basis | +|---|---:|---| +| Node.js | 24.18.0 LTS (Krypton) | Development, Docker, CI, and production target | +| npm | 11.16.0 | Version shipped with the pinned Node line | +| Payload | 3.86.0 | Exact package version | +| `@payloadcms/next` | 3.86.0 | Must exactly match Payload | +| `@payloadcms/db-postgres` | 3.86.0 | Must exactly match Payload | +| Next.js (Payload service only) | 15.4.11 | Explicitly accepted by `@payloadcms/next@3.86.0` peer range | +| React / React DOM | 19.1.0 | Accepted by Next.js 15.4.11 and conservative relative to the existing app | + +The existing frontend's Next.js 15.3.2 is outside Payload 3.86.0's accepted ranges. This is another reason to keep Payload in a dedicated workspace. Package metadata was checked with exact-version `npm view`; the official Payload installation documentation lists 15.4.11 as an accepted line. Node 26 on the host is not a target runtime and must not be used to create authoritative lockfiles or release builds. + +References: + +- https://payloadcms.com/docs/getting-started/installation +- https://payloadcms.com/docs/database/postgres +- https://payloadcms.com/docs/database/migrations +- https://nodejs.org/en/blog/release/v24.18.0 + +## Schema lifecycle + +Schema push is permitted only while prototyping against the disposable database. Before the first authoritative rehearsal: + +1. Disable push in the PostgreSQL adapter. +2. Commit a generated initial Payload migration. +3. Keep four separate commands with single responsibilities: + - `migration:reset`: destructively empties only the disposable destination. + - `migration:schema`: applies committed Payload/Drizzle migrations. + - `migration:import`: imports source records and files. + - `migration:validate`: performs read-only cross-system checks. +4. Define `migration:rehearse` only as deterministic composition of those commands. + +Database credentials are loaded by database-specific scripts only. Installation, builds, linting, and type checking do not read `.migration.env`. + +## Collection strategy + +Payload will expose first-class collections for: + +- users and media; +- buying-guide categories, subcategories, and entries; +- hazards and material taxonomy; +- materials, coatings, colors, opacity, status, and their junctions; +- laser sources, software, scan lenses, focus lenses, configurations, apertures, expanders, and diameters; +- fiber, UV, CO2 galvo, and CO2 gantry settings; +- projects and project files; +- user rigs, rig types, preferences, memberships, and claims. + +Directus junction tables become Payload relationships where doing so preserves ordering and ownership semantics. A junction remains an explicit collection when it carries its own meaningful fields or permissions. + +## ID preservation decision + +Source identifiers will be preserved as the actual Payload document IDs, not merely copied into an auxiliary legacy field. + +- UUID primary keys remain UUID/string IDs. +- Integer and medium-integer primary keys remain numeric custom IDs. +- Collections whose Directus primary key is named `submission_id` use that numeric value as Payload's custom `id`; the compatibility layer returns it as `submission_id` to existing consumers. +- Payload's PostgreSQL adapter will enable `allowIDOnCreate` for controlled migration imports. +- Every relationship is imported using the preserved target ID, avoiding translation tables and making verification direct. +- New numeric IDs must begin above the migrated maximum. Sequence values are explicitly advanced after import. +- The import rejects duplicate, null, lossy, or out-of-range identifiers before inserting records. + +This mixed per-collection custom-ID approach is preferable to globally converting every integer to UUID, which would break public URLs, foreign keys, and API assumptions. Payload officially supports explicit custom ID fields, and the PostgreSQL adapter supports IDs supplied during controlled create operations. + +References: + +- https://payloadcms.com/docs/fields/overview#custom-id-fields +- https://payloadcms.com/docs/database/postgres + +## File strategy + +All 535 database-referenced originals and all 2,411 currently unreferenced physical files remain retained. Referenced files become Payload media documents while preserving the Directus file UUID, original filename, MIME type, size, timestamps, ownership where valid, and checksum. Derived variants are classified separately from originals. + +Unreferenced files are not imported as editorial media until classified, but they remain in a migration-retention manifest with path, size, type, and checksum. No file is deleted by reset or migration tooling. + +The repository's separate curated file library (`app/files`, 282 files and approximately 461 MB at the baseline) is not Directus upload storage. It remains a distinct, read-only application asset volume in normal operation. Payload media and retained migration uploads use separate volumes and permissions. + +## Production promotion rule + +CI builds the frontend, Payload, BGBye, and proxy images once from reviewed commits under their pinned runtimes. Integration tests run those exact digests. Staging and production consume the same core Compose definitions and immutable image digests; they do not rebuild from source or use separately maintained service definitions. Environment overlays may change secrets, hostnames, resource policies, external storage/database endpoints, replica counts, and production TLS integration, but not application image contents or service contracts. + +Production database schema changes use committed Payload migrations as an explicit release job before application rollout. Imports are migration-only jobs and are not part of ordinary application startup. Promotion requires recorded image digests, migration status, validation results, rollback compatibility, and health checks. + +## Directus metadata disposition + +See `metadata-disposition.md` for the explicit include, translate, archive-only, and exclude decisions. Exclusion means “not imported into Payload runtime tables,” never “destroyed from the frozen snapshot or archive.” diff --git a/docs/payload-migration/environment-parity.md b/docs/payload-migration/environment-parity.md new file mode 100644 index 0000000..1f4ad74 --- /dev/null +++ b/docs/payload-migration/environment-parity.md @@ -0,0 +1,274 @@ +# Milestone 0: reproducible application environment + +Status: proposal for review. It describes future files and services; it does not authorize production changes or privileged host configuration. + +## Complete baseline runtime inventory + +The inventory is based on commit `4884b5d`, its checked-in Compose/Dockerfiles, application routes, Directus deployment exports, and the frozen structural migration manifests. + +| Component | Responsibility and dependencies | Persistence / lifecycle | +|---|---|---| +| `frontend` | Existing Next.js UI, middleware, server-rendered public pages, authenticated portal, compatibility APIs, file browser, SVGnest iframe, upload validation, Ko-fi webhook and claim endpoints, and BGBye proxy | Stateless image; Next cache on tmpfs; reads curated files; uses secure HTTP-only auth cookies | +| `payload` | Dedicated Payload admin, auth, content/media API, access control, hooks, jobs, and migrations | PostgreSQL plus Payload media volume; independent image and release lifecycle | +| `postgres` | Payload relational database | Named volume in nonproduction; managed/dedicated storage may replace it in production | +| `bgbye` | FastAPI image/video background removal with CUDA models, FFmpeg video processing, status polling, and ten-minute temporary-file cleanup task | Model caches are persistent; video/frame/output workspace is ephemeral; GPU is optional only if a functional CPU test profile is implemented | +| `proxy` | Single browser entrypoint, local TLS, host routing, forwarded headers, redirects, body limits, timeouts, and security headers | Configuration is versioned; certificates are mode-specific and never baked into application images | +| `mailpit` | Captures registration, recovery, verification, supporter-claim, and notification mail in nonproduction | Disposable by default; optional named volume for debugging; never deployed as the production relay | +| `migration` job image | Structural inventory, schema application, source import, file copy, sequence repair, checksums, and reports | Reads authorized source mount read-only; writes disposable PostgreSQL/media and non-personal summary reports | +| `validation` job image | Read-only source/destination comparison: counts, IDs, relationships, timestamps, ownership, permissions, files, checksums, and API contracts | No source writes; generated detailed reports are local and excluded from Git/build contexts | +| `e2e` job image | Browser and API tests against proxy-visible hostnames | Ephemeral browser artifacts; traces/screenshots are excluded when they may contain personal data | +| optional `legacy-directus` | Local reference for Studio presentation, permissions, flows, relations, files, and legacy API shapes | Uses a fresh writable MariaDB clone and copied uploads; never connects to the authoritative read-only source | +| optional `legacy-mariadb` | Writable database solely for the legacy reference service | Disposable named volume populated from the scrubbed dump | +| `fixture-seed` job | Loads deterministic synthetic users, content, ownership cases, and representative tiny files | Runs only against disposable fixture PostgreSQL/media | +| curated file library | Existing `app/files` manuals, specifications, software/reference files, and file-server routes | Separate read-only volume in runtime; approximately 461 MB / 282 files at baseline | +| Payload media | Migrated Directus uploads and future CMS uploads | Dedicated persistent volume/object-storage contract; not mixed with curated files | +| retained unreferenced uploads | 2,411 files pending classification | Read-only retention input; never deleted by application reset/import jobs | +| external Ko-fi | Sends signed webhook events and is the source for historical membership backfill | Nonproduction uses fixtures; no real webhook secret or outbound calls | +| production SMTP | Sends account and notification mail | Replaced by Mailpit in every nonproduction mode; credentials only in production secret scope | +| reverse-proxy/TLS provider | Public TLS and routing | Production-specific infrastructure around the same proxy contract | + +### Existing application processes and integrations + +- Authentication currently uses `ma_at`, `ma_rt`, `ma_ra`, and `ma_v` cookies, middleware redirects, periodic token validation, username-to-email lookup, recent-auth confirmation, registration, login, logout, and password change. Payload must add recovery and verification endpoints and mail templates because the baseline UI does not yet provide a complete recovery journey. +- Public/catalog consumers cover laser sources, four settings families, materials/coatings/hazards, buying guides, and projects. +- Authenticated consumers cover profile/avatar/password, projects and attachments, rigs, settings submission/deletion, claims, memberships/supporter badges, and account linking. +- Upload paths include avatars, project hero/attachments, settings photos/screenshots, Payload media, curated file downloads, and background-removal inputs/outputs. +- `app/scripts/backfill-kofi.mjs` is a remaining Directus consumer and becomes an idempotent Payload membership import job. +- `/api/webhooks/kofi`, supporter claim start/verify/unlink, and badge derivation become Payload-backed compatibility routes. Nonproduction uses signed fixture events only. +- Directus's two event flows (`unique_key_claims`, `claim_auto_assign`) must be reconstructed as transactional Payload hooks/jobs after behavior is resolved in the isolated legacy reference. +- BGBye has an in-process startup cleanup loop every ten minutes. There is no verified host cron/systemd job in the repository. Future scheduled Payload work must be an explicit worker/job service, not an undocumented host cron. +- SVGnest is vendored static client code embedded through the frontend. The separate `dashboard/` is a static legacy/operations dashboard in the monorepo but is not in the baseline Compose runtime; Milestone 0 must either add an explicit static service/profile or formally classify it as non-required. Default proposal: keep it as an optional `dashboard` profile, not part of the public application critical path. +- External links such as image hosting, paste, Forgejo, and community/support sites are navigation dependencies, not services required for the core stack to start. Smoke tests verify links are rendered but do not require those external systems. + +## Common Compose architecture + +The repository will use a project-name-scoped Compose model: + +```text +infra/compose/compose.yaml # core networks, services, health contracts +infra/compose/compose.dev.yaml # bind mounts/watch/debug ports +infra/compose/compose.snapshot.yaml # authorized read-only snapshot mounts +infra/compose/compose.staging.yaml # staging hosts/resources/secrets/storage +infra/compose/compose.production.yaml # production hosts/resources/external infrastructure +infra/compose/profiles/*.yaml # legacy Directus, GPU/CPU, dashboard, test tooling +``` + +Core service definitions and Dockerfiles are shared. Mode selection composes files and profiles rather than copying service definitions. + +| Mode | Services/data | Intended use | +|---|---|---| +| `fixture` | proxy, frontend, Payload, PostgreSQL, Mailpit, BGBye, fixture seed; optional dashboard | Daily development, CI, API tests, deterministic browser tests | +| `snapshot` | Same application images plus read-only scrubbed source/uploads and import/validation jobs | Realistic edge cases and file/API compatibility checks | +| `rehearsal` | Snapshot topology with destination reset, committed schema migration, full import, validation, then smoke/e2e gates | Repeatable cutover rehearsal; destructive only to project-scoped destination volumes | +| `staging` | Exact promoted image digests, staging PostgreSQL/media/secrets, capture or allow-listed staging SMTP | Production-shape acceptance and release candidate verification | +| `production` | Exact accepted digests, production PostgreSQL/media/SMTP/TLS/resources | Live service; no source snapshot, legacy Directus, fixture seed, Mailpit, or migration credentials mounted | + +Compose health dependencies use explicit checks rather than startup order: + +- proxy: configuration valid and HTTPS route responds; +- frontend: `/api/health/live` and `/api/health/ready` (Payload/BGBye dependency status separated); +- Payload: liveness plus database-backed readiness; +- PostgreSQL: `pg_isready`; +- BGBye: existing `/health`, expanded to distinguish model readiness; +- Mailpit: HTTP health endpoint; +- jobs: finite exit status and machine-readable summary. + +No application service receives `.migration.env`. Only explicitly invoked inventory/import/validation jobs receive migration variables. Package installation, linting, builds, unit tests, fixture mode, frontend, Payload, proxy, Mailpit, and BGBye do not load it. + +## Two nonproduction data modes + +### Fixture mode + +- A small, deterministic, non-personal dataset committed as factories/fixtures. +- Stable IDs exercise numeric IDs, UUIDs, ownership, public/private content, junctions, nulls, Unicode, timestamp boundaries, and authorization failures. +- Tiny generated fixtures cover JPEG, PNG, WebP/AVIF where supported, SVG rejection/handling, PDF, ZIP, plain text, and an executable-signature rejection case. +- Synthetic mailbox addresses use reserved domains and never identify real users. +- Resets remove only Compose-project-scoped PostgreSQL, media, mail, and temporary volumes. + +### Snapshot mode + +- Uses the credential-scrubbed MariaDB endpoint and authorized source/upload mounts read-only. +- Uses the same frontend, Payload, proxy, BGBye, and job images as fixture mode. +- Destination PostgreSQL and copied destination media are disposable and writable. +- Detailed validation artifacts stay local and ignored; committed reports contain structural summaries only. +- Outbound SMTP is forced to Mailpit and outbound network policy denies real SMTP/OAuth/Ko-fi delivery. + +## Optional legacy Directus reference + +The `legacy-reference` profile is local-only and off by default: + +1. Verify the archived Directus image by immutable image ID/digest before use; never pull `latest` as a substitute. +2. Create a new project-scoped MariaDB volume and restore the scrubbed dump into it. +3. Copy uploads into a new writable project-scoped volume; never mount `.migration-source/uploads` writable. +4. Generate a new local `KEY`, `SECRET`, administrator address/password, and any test policy tokens. None may match production values. +5. Disable SMTP, OAuth, external webhooks, telemetry where supported, public registration, and outbound network access. +6. Bind only to the internal Compose network and route through the local proxy/admin hostname; do not publish its container port publicly. +7. Label the UI and responses clearly as scrubbed, disposable, local reference data. + +This service is evidence, not an authoritative source. The frozen read-only MariaDB snapshot remains the migration authority. The archived image currently identified in the manifest is `sha256:dd5537…0801ce2a`; the complete value remains in the local manifest and must be supplied through an authorized local image import, not copied into a public registry without approval. + +## Reverse proxy and browser parity + +Proposed local endpoints use wildcard loopback names without editing DNS: + +- `https://app.le.localhost:8443` — frontend and compatibility APIs; +- `https://cms.le.localhost:8443` — Payload admin/API; +- `https://mail.le.localhost:8443` — Mailpit UI; +- optional `https://legacy.le.localhost:8443` — isolated Directus reference. + +The versioned proxy configuration must: + +- set `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto=https`, `X-Forwarded-Port`, `X-Real-IP`, and `X-Forwarded-For` consistently; +- support upgrade headers where an application endpoint needs them; +- reproduce legacy redirects and canonical-host behavior; +- set explicit per-route request limits: at least the verified historical 100 MB proxy ceiling, with lower application limits retained for avatars/settings/projects/BGBye; +- use 300-second upstream timeouts only where file/background processing requires them, with shorter defaults elsewhere; +- preserve streaming/download headers and range behavior for representative files; +- run application containers with fixed non-root UIDs and preflight media/file permissions; +- derive cookie `Secure`, `HttpOnly`, `SameSite`, domain, path, expiry, refresh, and recent-auth behavior from one shared auth contract. + +Local HTTPS is required to test secure cookies. A repository-controlled development CA/certificate generator may create ignored local certificates, but trusting that CA in the host browser is a host change and requires user approval. Port 8443 avoids privileged rootless port binding; cookie semantics do not depend on port. Production uses the same routing contract with its real TLS termination and standard port 443. + +## Safe email + +- Mailpit is the only SMTP host supplied to fixture, snapshot, rehearsal, and default staging modes. +- Nonproduction secrets/config validation fails closed if an SMTP hostname, sender domain, or port matches a production allow/deny signature. +- Egress policy blocks TCP 25/465/587 from application containers in snapshot/rehearsal modes except to the internal Mailpit service. +- Tests retrieve messages through Mailpit's API and follow recovery/verification links through the local HTTPS proxy. +- Snapshot users are never sent messages automatically. Tests create synthetic accounts or redirect recipients at the mail transport boundary. +- Production SMTP credentials are provided only by the production secret mechanism and are never stored in Compose YAML, `.env` examples, images, test traces, or migration reports. + +## Rootless Codex-controlled runtime + +Required target: a dedicated rootless Docker daemon owned by the separate `le_payload_dev` account. Future Codex migration sessions must execute as that account (preferred) or through a narrowly scoped account transition; the daemon socket is not shared broadly. Rootless Podman is an acceptable fallback only after Compose/profile behavior is proven; Docker is preferred because the repository and production already use Compose. + +Codex must be able to build images, create/remove only its project containers/networks/volumes, inspect their logs, reset disposable PostgreSQL, run finite jobs, and execute integration/browser tests. It must not access `/var/run/docker.sock`, the production daemon/context, production containers/volumes/networks, `/var/www`, or `/srv/directus-archive`. + +Current host observation: + +- `sol6_vi` is not in the Docker group but can read the production application's world-readable environment files under `/var/www`; +- Docker and Docker Compose clients exist; +- no Podman client is installed; +- the default Docker context does not provide a usable daemon/security result to this user; +- no enabled user-level rootless Docker or Podman service was detected. + +Therefore a `sol6_vi`-owned daemon is rejected: it could bind-mount those readable production secrets. Milestone 0 requires the isolated account and host changes described in `host-runtime-plan.md`, and work must stop before performing them. An administrator must approve/provision: + +1. dedicated `le_payload_dev` identity and restricted filesystem ACLs; +2. rootless prerequisites (`newuidmap`/`newgidmap`, subordinate UID/GID ranges, rootless networking and overlay helpers as required by the OS); +3. rootless Docker daemon installation for `le_payload_dev` using its dedicated data root and named context; +4. optional user-service lingering if stacks must survive logout; +5. deterministic CPU BGBye first; optional GPU access only after core acceptance; +6. user/browser-profile-scoped development CA trust; +7. capacity limits for images, caches, snapshots, media, PostgreSQL, and test artifacts. + +No production socket forwarding, Docker-group membership, broad sudo rule, daemon TCP exposure, or bind mount into production paths is acceptable. + +## One-command workflows + +The repository-root commands will be thin, reviewed wrappers around Compose and finite job containers: + +```text +./scripts/stack-up fixture +./scripts/stack-up snapshot +./scripts/stack-down +./scripts/reset +./scripts/schema +./scripts/import +./scripts/validate +./scripts/smoke-test +``` + +Each wrapper validates the selected rootless context, project name, mode, required mounts, and forbidden production paths before doing work. `reset` requires an explicit nonproduction project label and refuses unknown database hosts. `stack-down` does not delete volumes by default. A separate explicit disposable purge command may remove only labeled project volumes. + +From a clean checkout, fixture mode requires only the rootless runtime, ignored local secrets generated from examples, and cached/built images. Snapshot mode additionally requires the authorized read-only source path and migration environment. There are no undocumented restore, chmod, hostname, secret, or database steps. + +## Build context and secret isolation + +Every context (`app`, `payload`, `bgbye`, proxy, jobs/tests, and repository root where used) excludes: + +- `.git`, `.worktrees`, `.migration-source`, `.migration.env`; +- `*.sql`, `*.sql.gz`, database dumps and snapshot manifests containing data; +- source and destination uploaded user files; +- migration-generated detailed reports/logs; +- browser traces/screenshots/videos from snapshot mode; +- local secrets, certificates/private keys, dependency trees, and build outputs. + +Build verification inspects image histories and filesystems for forbidden names and secret fingerprints. Compose configuration tests assert that migration mounts/variables appear only on authorized finite jobs and never in production output. + +## Proposed final repository layout + +```text +/ +├── apps/ +│ ├── web/ # current Next.js frontend + compatibility APIs +│ └── payload/ # dedicated Payload admin/API +├── services/ +│ ├── bgbye/ # FastAPI GPU/CPU worker +│ ├── proxy/ # versioned local/production routing contract +│ └── dashboard/ # optional static dashboard profile +├── packages/ +│ ├── contracts/ # API DTOs, auth/cookie, errors, health contracts +│ ├── config/ # validated environment schemas, no secret values +│ ├── fixtures/ # deterministic synthetic factories and tiny files +│ └── test-helpers/ # API/browser/mail/file helpers +├── migration/ +│ ├── inventory/ # structural inventory tooling +│ ├── mappings/ # reviewed field/relationship mappings +│ ├── schema/ # committed Payload/Drizzle migrations +│ ├── import/ # idempotent source/file import +│ ├── validation/ # counts, relationships, ownership, checksums +│ └── legacy-reference/ # isolated restore/bootstrap tooling +├── infra/ +│ ├── compose/ # core and mode overlays/profiles +│ ├── proxy/ # routes, headers, limits, local TLS config +│ ├── health/ # healthcheck commands/contracts +│ └── env/ # non-secret examples and mode schemas +├── scripts/ # one-command wrappers and safety guards +├── tests/ +│ ├── unit/ +│ ├── integration/ +│ ├── contracts/ +│ ├── e2e/ +│ └── fixtures/files/ +├── docs/payload-migration/ +├── directus/ # retained legacy evidence during migration +├── .nvmrc / .node-version +└── Dockerfiles remain beside each build context +``` + +The current `app`, `bgbye`, and `dashboard` directories will be moved only in a dedicated mechanical commit after build-context and path references are covered by tests. Milestone 0 may initially implement the target logical boundaries without a risky all-at-once directory move. + +## End-to-end test matrix + +| Area | Fixture mode | Snapshot/rehearsal mode | Core assertions | +|---|---|---|---| +| Registration | Full | Synthetic account only | validation, duplicate handling, role/status, captured verification mail | +| Login/logout | Full | Transitional-hash synthetic/test account; no personal output | username/email login, invalid credentials, refresh/expiry, cookie clearing | +| Password recovery | Full | Synthetic recipient only | enumeration resistance, token expiry/single use, Mailpit link, new login | +| Cookies/sessions | Full browser matrix | Full | Secure/HttpOnly/SameSite/path/domain, proxy scheme, redirects, refresh, recent-auth | +| Account/profile | Full | Structural/authorized test account | self-read/update, sensitive re-auth, avatar, cross-user denial | +| Projects | CRUD/files | Counts/contracts/representative records | ownership, public reads, attachment links, delete boundaries | +| Rigs | CRUD | Counts/contracts | owner presets and update/delete isolation | +| Machine settings | Four families | Full counts/relationships/API parity | create/read/update/delete ownership, images, `submission_id` compatibility | +| Claims/memberships | Full state fixtures | Flow/API parity | unique key, auto-assignment, pending transitions, supporter visibility | +| Upload/download | Representative file matrix | Realistic files/checksums | signatures, size limits, names, MIME, ranges, traversal prevention, permissions | +| Admin | Full | Read/controlled test writes | admin login, collections, roles, file management, forbidden member access | +| Background removal | Tiny image; deterministic allowed method | Representative image only | proxy body limit, model readiness, output PNG, invalid method/file, cleanup | +| Public utilities | Full | Full | toolkit, SVGnest, file browser, legacy redirects, external-link rendering | +| Compatibility APIs | Contract fixtures | Response-shape differential tests | Directus-like filters/fields/sort/pagination/errors where still consumed | +| Email | Mailpit | Mailpit only | no external delivery, correct recipient rewrite, recovery/verification/support flows | +| Authorization | Full adversarial matrix | Selected real relationship topology | anonymous/member/owner/admin/bot boundaries and field-level restrictions | +| Health/restart | Full | Full | dependency readiness, graceful restart, persistent DB/media, ephemeral temp data | + +Snapshot test logs report identifiers only as one-way test-local aliases or aggregates; they do not emit emails, usernames, hashes, record bodies, or user-provided content. + +## Promotion to staging and production + +1. CI under Node 24.18.0 builds each application image once and records its digest and source commit. +2. Fixture integration/e2e tests run those digests through the common core Compose model. +3. Authorized snapshot rehearsal runs the same digests with read-only source mounts and disposable destinations. +4. Staging references the same digests and core services plus staging overlay values; it does not rebuild. +5. A release manifest records digests, committed database migration set, config schema version, validation summaries, and rollback compatibility. +6. Production references that release manifest and production overlay. Schema migration is a distinct gated job; application rollout follows readiness checks. +7. Rollback changes image digests and only rolls schema back when the committed migration explicitly supports it. Source archives and retired Directus remain outside this deployment model until separately authorized for retirement. diff --git a/docs/payload-migration/host-runtime-plan.md b/docs/payload-migration/host-runtime-plan.md new file mode 100644 index 0000000..9e680a3 --- /dev/null +++ b/docs/payload-migration/host-runtime-plan.md @@ -0,0 +1,291 @@ +# Milestone 0 host runtime and security plan + +Status: planning-only. Commands in this document are proposed for administrator review. None have been executed. + +## Permissions-only access audit + +The audit inspected only existence, filesystem type, owner/mode, effective read/write/traverse permission, Docker context endpoints, and socket metadata. It did not read, print, copy, or inspect secret values, production databases, container metadata, or archive contents. + +| Boundary | Effective access for `sol6_vi` | Finding | +|---|---|---| +| `/var/www` | read/traverse, not write | Production tree names and readable files are exposed to the account | +| `/var/www/lasereverything.net.db` and `app/` | read/traverse, not write | Production application and deployment configuration are readable | +| production `.env.local` and BGBye `.env` | read, not write; mode `0644` | **Unsafe for a `sol6_vi`-owned rootless daemon**; values were not read | +| `/var/lib/docker` | no read/write/traverse | Production Docker data is denied by host permissions | +| `/var/lib/mysql` | no read/write/traverse | Production database storage is denied | +| `/var/run/docker.sock` and `/run/docker.sock` | no read/write | Production daemon is denied; only the `default` context points to it | +| `/srv/directus-archive` | no read/write/traverse | Root-only archive boundary works; timestamped child is inaccessible | +| `/srv/codex-migration` | read/traverse, not write | Scrubbed migration source is available read-only | +| `/srv/codex-secrets` | read/traverse at directory level, not write | Migration env is readable as intended; access must be narrowed for the new account to the one nonproduction file | +| workspace | read/write | Current development tree is owned by `sol6_vi` | + +Conclusion: rootless Docker owned by `sol6_vi` would protect the production daemon but not production files already readable to that user. Any controller of that daemon could mount the readable production environment files. The production secret file modes also deserve independent remediation. + +## Runtime account model comparison + +| Model | Advantages | Risks / cost | Decision | +|---|---|---|---| +| `sol6_vi` owns rootless Docker | Minimal provisioning; current workspace already writable | Can bind-mount world-readable production secrets and application files; mixes general Codex work with container authority | Rejected on this host | +| dedicated `le_payload_dev` | Daemon owner has an explicit filesystem allow-list; project storage and cleanup are attributable; production paths can be proven inaccessible | Requires account, ACL, subordinate IDs, separate Git credentials/session, storage, and user service | **Recommended** | + +Future autonomous migration work should run in a Codex session whose OS identity is `le_payload_dev`. Do not grant `sol6_vi` generic access to the development daemon socket as a convenience; that weakens accountability. If orchestration must be initiated from another account, use a narrowly audited command wrapper or SSH into `le_payload_dev`, not a shared Docker group and not TCP exposure of the daemon. + +## Exact proposed identity and filesystem model + +Proposed fixed identity: + +- user and primary group: `le_payload_dev`; +- UID/GID: `1200` (verified unused at audit time; recheck immediately before creation); +- shell/home: `/bin/bash`, `/home/le_payload_dev`; +- no supplementary production groups, no `docker` group, no sudo/wheel membership; +- subordinate UID and GID range: `165536:65536`, non-overlapping with the observed `sol6_ii:100000:65536` allocation; +- dedicated project root: Btrfs subvolume `/srv/le-payload-dev`, owner `1200:1200`, mode `0700`; +- workspace: `/srv/le-payload-dev/workspace/lasereverything.net.db`; +- daemon data root: `/srv/le-payload-dev/runtime/docker`; +- runtime socket: `/run/user/1200/docker.sock`; +- Docker client/config: `/home/le_payload_dev/.docker` and `/home/le_payload_dev/.config/docker`; +- Compose project prefix: `le_payload_dev`, enforced by wrapper preflight. + +Only the dedicated account receives: + +- read/write access to its workspace and runtime/storage subtree; +- read/traverse ACL on the frozen scrubbed source only; +- read ACL on `/srv/codex-secrets/le-payload-migration.env` only; +- a separate nonproduction Git credential/deploy key with access limited to the application repository. + +It receives no ACL or group membership for `/var/www`, production database paths, production sockets/volumes, or `/srv/directus-archive`. + +## Exact privileged prerequisites and commands + +These commands are a reviewable proposal for an administrator on Garuda/Arch Linux. Recheck UID/GID and subordinate ranges first. Do not paste secrets into the shell history. + +```bash +# 1. Recheck proposed IDs/ranges are unused. +getent passwd 1200 +getent group 1200 +grep -E '^(le_payload_dev|[^:]+:165536:65536)$' /etc/subuid /etc/subgid + +# 2. Install rootless prerequisites. docker-rootless-extras is an Arch AUR +# package and must be built/reviewed under the administrator's normal AUR policy. +sudo pacman -S --needed rootlesskit slirp4netns fuse-overlayfs shadow +# Then install the reviewed docker-rootless-extras AUR package by the host's +# approved AUR procedure; do not curl a remote install script into a shell. + +# 3. Create the isolated identity. +sudo groupadd --gid 1200 le_payload_dev +sudo useradd --uid 1200 --gid le_payload_dev --create-home \ + --home-dir /home/le_payload_dev --shell /bin/bash le_payload_dev +sudo usermod --add-subuids 165536-231071 --add-subgids 165536-231071 le_payload_dev + +# 4. Create a quota-capable project subvolume and ownership boundary. +sudo btrfs subvolume create /srv/le-payload-dev +sudo chown 1200:1200 /srv/le-payload-dev +sudo chmod 0700 /srv/le-payload-dev +sudo -u le_payload_dev mkdir -p \ + /srv/le-payload-dev/workspace \ + /srv/le-payload-dev/runtime/docker \ + /srv/le-payload-dev/data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ + /srv/le-payload-dev/artifacts/{browser,migration-reports} \ + /srv/le-payload-dev/secrets + +# 5. Apply a 250 GiB ceiling to the project subvolume. +sudo btrfs quota enable /srv +sudo btrfs qgroup limit 250G /srv/le-payload-dev + +# 6. Grant only source traversal/read and one migration secret file. +# Recheck the resolved `current` target before applying ACLs. +readlink -f /srv/codex-migration/current +sudo setfacl -m u:le_payload_dev:--x /srv/codex-migration +sudo setfacl -R -m u:le_payload_dev:r-X /srv/codex-migration/source-20260710-175408 +sudo setfacl -m u:le_payload_dev:--x /srv/codex-secrets +sudo setfacl -m u:le_payload_dev:r-- /srv/codex-secrets/le-payload-migration.env + +# 7. Remediate production secret readability separately. +# Root-owned Compose can still read mode 0600 files. Confirm the production +# operator before applying because this changes production file permissions. +sudo chown root:root /var/www/lasereverything.net.db/app/.env.local +sudo chmod 0600 /var/www/lasereverything.net.db/app/.env.local +sudo chown root:root /var/www/lasereverything.net.db/bgbye/.env +sudo chmod 0600 /var/www/lasereverything.net.db/bgbye/.env + +# 8. As le_payload_dev, configure the rootless daemon with an explicit data root. +sudo -iu le_payload_dev mkdir -p ~/.config/docker +# Administrator writes ~/.config/docker/daemon.json as documented below, +# then the dedicated user enables the packaged rootless user service. +sudo -iu le_payload_dev systemctl --user enable --now docker.socket + +# 9. Optional only if reviewed stacks must survive logout. +sudo loginctl enable-linger le_payload_dev +``` + +Proposed `/home/le_payload_dev/.config/docker/daemon.json`: + +```json +{ + "data-root": "/srv/le-payload-dev/runtime/docker", + "live-restore": false, + "log-driver": "local", + "log-opts": { "max-size": "20m", "max-file": "5" } +} +``` + +The exact Arch rootless unit name (`docker.service` versus `docker.socket`) must be verified from the reviewed package before enabling. Cgroup v2 is present; after provisioning, verify the rootless/security options and delegated controllers before relying on CPU, memory, or PID limits. + +## Required positive and negative verification + +Run as `le_payload_dev`, printing only permissions/status: + +```bash +test ! -r /var/www/lasereverything.net.db/app/.env.local +test ! -x /var/lib/docker +test ! -x /var/lib/mysql +test ! -r /var/run/docker.sock +test ! -x /srv/directus-archive +test -r /srv/codex-migration/source-20260710-175408/README.txt +test ! -w /srv/codex-migration/source-20260710-175408/README.txt +test -r /srv/codex-secrets/le-payload-migration.env +test ! -w /srv/codex-secrets/le-payload-migration.env +DOCKER_HOST=unix:///run/user/1200/docker.sock docker info --format '{{json .SecurityOptions}}' +``` + +The final line must show rootless security. The development context must resolve only to `/run/user/1200/docker.sock`; wrappers reject `default`, `/var/run/docker.sock`, TCP endpoints, SSH contexts, and any unknown endpoint. + +## Production path and socket denial preflight + +Every stack/job wrapper will fail before Compose evaluation if: + +- effective UID is not the approved dedicated UID; +- Docker endpoint is not exactly the approved rootless Unix socket; +- Compose project name lacks the fixed nonproduction prefix; +- any resolved bind source is `/var/www`, `/var/lib/docker`, `/var/lib/mysql`, `/srv/directus-archive`, `/var/run`, `/run/docker.sock`, or a descendant; +- any non-finite application service receives `.migration.env`, `.migration-source`, a MariaDB source URL, or migration credentials; +- destination database host/port is not the approved nonproduction target/service; +- production-named secret scopes, SMTP hosts, OAuth variables, Ko-fi secrets, or webhook credentials appear in fixture/snapshot/rehearsal rendered Compose; +- reset/purge targets a bind mount, external volume, unknown project label, or any path outside `/srv/le-payload-dev`. + +Tests render each Compose mode and inspect mounts, secrets, environment variable **names/classifications**, networks, contexts, labels, and volume ownership without printing values. + +## Authoritative migration-source protection + +- The authoritative MariaDB remains read-only at the database privilege and filesystem layers. +- `.migration-source` and `.migration.env` are mounted read-only only into finite `inventory`, `import`, and `validation` jobs. +- Frontend, Payload, BGBye, proxy, Mailpit, fixture seed, package installation, build, lint, unit, integration, and ordinary browser-test services never receive those mounts or variables. +- Legacy Directus restores the scrubbed SQL into a separate project-scoped writable MariaDB volume and copies uploads into `/srv/le-payload-dev/data/legacy-uploads` or a project volume. +- Legacy Directus never mounts the source database storage or uploads, even read-only; the finite clone job is the only bridge. +- Reset/purge operates only on labeled nonproduction destinations. It does not follow source symlinks and refuses all external/bind volumes. +- `/srv/directus-archive` remains inaccessible and is never mentioned as a mount source in executable Compose configuration. + +## Nonproduction storage plan + +The host currently has approximately 650 GiB available on the Btrfs filesystem containing `/srv` and `/home`. Proposed aggregate project quota: 250 GiB. + +| Path | Planning allowance | Ownership | Cleanup / backup | +|---|---:|---|---| +| `runtime/docker` | 80 GiB soft budget | `le_payload_dev` | prune only project/unused development images after digest recording; no backup | +| `data/postgres` | 25 GiB | `le_payload_dev` / rootless mapped IDs | disposable; reset by labeled volume; no backup | +| `data/media` | 15 GiB | fixed container UID mapped by rootless daemon | fixture disposable; snapshot copied data disposable; no authoritative backup role | +| `data/legacy-uploads` | 10 GiB | rootless mapped service UID | copied from scrubbed source; disposable; never source of truth | +| `data/bgbye-models` | 35 GiB | BGBye service/rootless mapping | checksummed cache; LRU/manual cleanup; redownloadable | +| `data/tmp` | 10 GiB | service-specific mapped UID | tmpfs or age-based cleanup; no backup | +| `artifacts/browser` | 5 GiB | `le_payload_dev` | TTL 7 days; snapshot artifacts treated as sensitive and never committed | +| `artifacts/migration-reports` | 10 GiB | `le_payload_dev` | detailed reports TTL 30 days; committed summaries structural only | +| workspace/metadata headroom | 10 GiB | `le_payload_dev` | Git remote is source backup; no personal exports committed | + +The listed soft budgets total 200 GiB. The 250 GiB hard qgroup ceiling reserves 50 GiB emergency headroom. Startup refuses when host free space is below 100 GiB, project usage exceeds 200 GiB, or required free space for a rehearsal cannot be reserved. The production filesystem is not used for spillover. + +## Production ingress boundary + +Production will use an **internal application-gateway container behind TITANSERVER's existing shared host reverse proxy**. + +- The server-wide proxy remains host-managed, terminates public TLS, owns ports 80/443, ACME, and cross-application routing. +- The repository-controlled gateway binds only to a dedicated host loopback port or private production network and routes frontend versus Payload internal services. +- The host proxy forwards approved public hostnames to that one gateway and overwrites—not trusts from clients—the forwarding chain and scheme headers. +- The application repository supplies a versioned host-proxy include/template and contract tests, but an operator explicitly reviews/renders/includes it. Containers never mount or edit host proxy configuration or certificates. +- The gateway is not published directly to the internet. Payload admin exposure can be separately restricted at the host proxy. +- Existing `forms.lasereverything.net` behavior remains under the shared ingress until cutover authorization. + +Local development uses its own gateway/TLS because it does not share production ingress. This design preserves server-wide ownership while keeping application routing rules testable and versioned. + +## Local HTTPS and CA trust + +The local gateway uses a repository-pinned proxy image and an ignored, generated development CA to serve: + +- `app.le.localhost`; +- `cms.le.localhost`; +- `mail.le.localhost`; +- optional `legacy.le.localhost`. + +All bind to loopback port 8443, avoiding privileged ports and host DNS changes (`*.localhost` resolves locally). Certificates and private keys live under `/srv/le-payload-dev/secrets/local-ca`, mode `0700/0600`, excluded from Git and build contexts. + +Automated Playwright tests may use a dedicated browser profile with its own trust database. Manual Chromium trust can be limited to `/home/le_payload_dev/.pki/nssdb` using the already installed `certutil`; system-wide CA trust is unnecessary. Importing the CA into even that user/browser profile is a host/user-state change and requires explicit approval. Removal deletes the certificate from that profile and the ignored CA directory. + +## Mailpit and outbound protection + +Mailpit exists only in fixture, snapshot, rehearsal, and default staging modes: + +- internal Compose networks only; UI reaches the browser through the local gateway; +- no public host bind and no production ingress route; +- SMTP relay/forwarding and webhook forwarding disabled; +- no production SMTP variables mounted; +- tests query Mailpit's internal HTTP API and follow local HTTPS verification/recovery links; +- registration, verification, recovery, supporter claim, and notification paths are covered. + +Defense in depth: + +1. Mode-specific environment schema rejects production SMTP hostnames/sender domains, ports 25/465/587 as external endpoints, production credential variable scopes, OAuth secrets, Ko-fi production tokens, and webhook credentials. +2. Runtime services attach only to an `internal: true` application network in nonproduction. Mail is addressed to the internal `mailpit:1025` service. +3. Snapshot-derived recipient addresses are rejected or rewritten at the transport boundary to deterministic reserved-domain aliases. Only synthetic fixture accounts may be addressed directly. +4. No production credential exists in rendered Compose, container environment, secret mounts, or image history. +5. A finite `egress-test` runs from the exact frontend and Payload runtime network namespaces: Mailpit delivery must succeed; connection attempts to controlled external test endpoints on SMTP ports must fail; DNS/HTTP behavior is recorded separately. + +Firewall-level isolation will not be claimed until these tests pass under the selected rootless runtime. If Docker `internal` networking still permits an undesired host/external route, the implementation stops and proposes an approved rootless egress control rather than asserting safety. Tests use an administrator-approved controlled endpoint, never a real third-party SMTP server. + +Production remains unchanged: application services use the existing self-hosted SMTP infrastructure and real recipients through production-only secrets and network policy. Mailpit never replaces or modifies it. + +## BGBye CPU/GPU profiles + +Milestone 0 starts with a deterministic CPU-compatible BGBye image/profile: + +- same FastAPI paths, methods endpoint shape, request fields, status codes, content types, health/readiness schema, and error contract as GPU; +- one pinned CPU-capable segmentation model for real tiny-image processing in routine tests; +- image upload/proxy limits, invalid method/file handling, alpha PNG output, concurrency serialization, temporary-file lifecycle, status polling, and FFmpeg contract can be exercised; +- model artifacts are pinned/checksummed and readiness fails until loaded. + +CUDA-only validation is deferred to an opt-in GPU profile/staging gate: + +- NVIDIA runtime/device visibility and driver compatibility; +- every production model/backend, CUDA placement, GPU lock/concurrency, memory cleanup/OOM behavior, performance envelope, and longer video processing; +- production model-cache checksum and offline startup behavior. + +CPU and GPU images share server contract tests. Capability differences appear in explicit health/method metadata rather than changing endpoints. A production GPU image is released only after model preload/readiness, representative image/video output, cleanup, restart, and resource tests pass on GPU staging. Rootless GPU provisioning does not block the first stack. + +## Rootless runtime rollback/removal + +Rollback must be run only after confirming the selected context/socket is the dedicated one: + +```bash +# As le_payload_dev: stop project resources, then the user daemon. +DOCKER_HOST=unix:///run/user/1200/docker.sock docker compose \ + --project-name le_payload_dev down --remove-orphans +systemctl --user disable --now docker.socket docker.service + +# As administrator: disable optional lingering. +sudo loginctl disable-linger le_payload_dev + +# Verify production daemon endpoint and data were never referenced by project metadata/logs. +# Use the planning preflight/audit script; do not connect to or mutate production Docker. + +# Remove only the dedicated project subvolume after an explicit artifact review. +sudo btrfs subvolume delete /srv/le-payload-dev + +# Remove subordinate allocations and account after the subvolume/runtime is gone. +sudo usermod --del-subuids 165536-231071 --del-subgids 165536-231071 le_payload_dev +sudo userdel --remove le_payload_dev +sudo groupdel le_payload_dev 2>/dev/null || true +``` + +The dedicated Docker context lives only in the dedicated user's home and disappears with that home. If another operator created a client context pointing to the user socket, remove that context explicitly from that operator's client configuration. + +Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Source ACL entries can be removed with `setfacl -x u:le_payload_dev` before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, the frozen source, or `/srv/directus-archive`. + +Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-payload-dev` resources were deleted. diff --git a/docs/payload-migration/implementation-plan.md b/docs/payload-migration/implementation-plan.md new file mode 100644 index 0000000..5fb35f9 --- /dev/null +++ b/docs/payload-migration/implementation-plan.md @@ -0,0 +1,82 @@ +# Payload transition implementation plan + +Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runtime approval gate. + +## Milestone 0 — complete reproducible application environment + +1. **Accepted architecture record** + - Preserve baseline `4884b5d`, dedicated Payload service, PostgreSQL, exact dependency matrix, mixed ID preservation, and committed-migration workflow. + - Review `architecture-proposal.md`, `environment-parity.md`, and host prerequisites. + +2. **Host runtime gate — requires human approval/provisioning** + - Provision the dedicated `le_payload_dev` account and a rootless Docker context owned only by it. + - Restrict production environment files currently readable to `sol6_vi`; verify the development account cannot traverse production paths. + - Use deterministic CPU BGBye initially; defer optional rootless GPU provisioning. + - Approve browser-profile-scoped local CA trust for HTTPS testing. + - Confirm nonproduction storage/capacity locations. + - Verify forbidden production paths and sockets are inaccessible. + - Execute every positive and negative preflight in `host-runtime-plan.md` before application work. + +3. **Repository/environment structure** + - Add common Compose core, mode overlays/profiles, environment schemas/examples, proxy configuration, health contracts, and safe defaults. + - Complete Docker/build-context exclusions for every context. + - Add one-command wrappers with context/path/project safety checks. + - Add automated external-SMTP denial and forbidden-mount/context tests; make snapshot startup fail closed. + +4. **Fixture stack** + - Build exact pinned images under Node 24.18.0. + - Bring up proxy, frontend, Payload shell, PostgreSQL, Mailpit, deterministic CPU BGBye profile, and fixtures. + - Prove HTTPS cookies, forwarded headers, uploads, mail capture, health, logs, reset, and teardown. + +5. **Snapshot/rehearsal stack** + - Mount only authorized scrubbed source and uploads read-only on finite jobs. + - Add disposable destination media/PostgreSQL and detailed-report exclusions. + - Add isolated legacy Directus reference profile using a writable clone and local-only credentials. + - Prove no production SMTP/OAuth/webhook/token configuration or outbound delivery. + - Keep authoritative source and `.migration.env` mounted only on finite inventory/import/validation jobs. + +6. **Milestone 0 acceptance** + - Clean-checkout fixture startup and smoke test require no undocumented steps. + - Snapshot startup passes mount/secret/egress safety assertions. + - Same image digests run in fixture and snapshot modes. + - Rootless user can control only its nonproduction project resources. + +## Milestone 1 — structural inventory and mapping + +- Generate structural-only collections, fields, relationships, indexes, policies, permissions, flows, operations, row counts, and application-consumer inventory. +- Commit reviewed field-by-field MariaDB-to-PostgreSQL mapping without record samples. +- Classify all files and Directus metadata; retain unresolved files. +- Record only genuine product decisions. + +## Milestone 2 — Payload schema and security model + +- Implement collections, custom IDs, indexes, relationships, media, users, roles, access functions, hooks, and jobs. +- Prototype with schema push only on disposable development PostgreSQL. +- Replace push with committed deterministic migrations before authoritative rehearsal. +- Implement captured email recovery/verification and shared cookie/session contract. + +## Milestone 3 — deterministic migration pipeline + +- Implement separate reset, schema, import, and validation jobs. +- Import in dependency order with preserved IDs/timestamps/ownership and repaired sequences. +- Preserve password hashes for compatibility research without logging them. +- Copy referenced media and retain/classify all unreferenced files. +- Make imports restartable/idempotent or fail safely with explicit checkpoints. + +## Milestone 4 — application compatibility and end-to-end behavior + +- Move each Directus consumer behind shared Payload/compatibility clients. +- Rebuild Ko-fi, claims, memberships, flows, and backfill behavior. +- Implement API contract comparisons and the full fixture/snapshot test matrix. +- Validate password-hash transitional login and forced rehash strategy. + +## Milestone 5 — full rehearsals and staging candidate + +- Run repeated clean destination rehearsals. +- Verify counts, IDs, relationships, timestamps, ownership, permissions, files, checksums, API behavior, and browser workflows. +- Produce aggregate/non-personal validation summaries and unresolved product decisions. +- Build immutable release images and promote their digests to staging. + +## Production boundary + +Stop for explicit authorization before production deployment, reverse-proxy/DNS changes, production secrets or SMTP use, email to real users, final write freeze/cutover, Directus restart/retirement, production database mutation, or archive destruction. diff --git a/docs/payload-migration/metadata-disposition.md b/docs/payload-migration/metadata-disposition.md new file mode 100644 index 0000000..f313cf2 --- /dev/null +++ b/docs/payload-migration/metadata-disposition.md @@ -0,0 +1,28 @@ +# Directus metadata disposition + +This inventory is structural and contains no example user records or user-provided content. + +| Directus area | Disposition | Reason | +|---|---|---| +| `directus_users` | Translate into Payload users | Preserve user IDs, supported profile fields, status, role mapping, timestamps, and password hashes pending compatibility findings | +| `directus_files`, `directus_folders` | Translate into media and folder metadata | Required to retain referenced files, hierarchy, checksums, timestamps, and ownership | +| `directus_collections`, `directus_fields`, `directus_relations` | Design input; do not import as runtime records | Re-expressed as committed Payload TypeScript collection definitions and migrations | +| `directus_roles`, `directus_policies`, `directus_access`, `directus_permissions` | Design input and verification fixtures | Re-expressed as Payload access functions and role fields; Directus policy rows have no runtime meaning in Payload | +| `directus_flows`, `directus_operations` | Translate behavior into hooks/jobs/endpoints | Flow graphs are implementation evidence, not portable executable definitions | +| `directus_settings` | Selective manual mapping | Only settings with a Payload or application equivalent are relevant | +| `directus_presets` | Archive-only pending editor review | UI bookmarks, filters, layouts, and per-user preferences are Directus Studio-specific | +| `directus_dashboards`, `directus_panels` | Archive-only | Directus Insights configuration is not portable and both source tables are empty | +| `directus_comments`, `directus_notifications` | Exclude from runtime | Empty in the scrubbed source; Directus collaboration internals | +| `directus_activity`, `directus_revisions` | Exclude from working migration | Intentionally scrubbed volatile audit history; untouched root-only archive remains authoritative | +| `directus_sessions` | Exclude | Intentionally scrubbed and cryptographically incompatible; all users receive new sessions | +| Static tokens and user `token` values | Exclude | Revoked credential material must never be migrated | +| TFA secrets, `auth_data` | Exclude | Intentionally scrubbed authentication secrets; users re-enroll if required | +| OAuth clients, codes, consents, tokens | Exclude | Empty/scrubbed live authorization material; providers must be configured afresh | +| `directus_shares` | Exclude | Scrubbed bearer-like public access grants must not survive cutover | +| `directus_migrations` | Exclude | Records Directus's schema history, not Payload's migration history | +| `directus_extensions` | Inspect source extension directory; do not import rows | Extension behavior must be reimplemented and reviewed; the metadata table is empty | +| deployment project/run tables | Archive-only | Directus deployment subsystem state has no Payload runtime equivalent | +| `directus_versions` | Exclude from initial runtime | Empty; Payload versioning will begin under its own schema if enabled | +| translations | Archive-only pending localization decision | Empty in source; no initial runtime effect | + +No exclusion authorizes deletion of source data, frozen files, manifests, or the root-only archive. diff --git a/payload/.dockerignore b/payload/.dockerignore new file mode 100644 index 0000000..47a4626 --- /dev/null +++ b/payload/.dockerignore @@ -0,0 +1,14 @@ +.migration-source +.migration.env +.worktrees +node_modules +.next +migration/generated +*.sql +*.sql.gz +uploads +media +test-results +playwright-report +blob-report +.auth From 27fd4f4ae38caebf08ae920ea396b6c43e732079 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 10 Jul 2026 20:21:27 -0400 Subject: [PATCH 04/14] define canonical migration deployment paths --- .../architecture-proposal.md | 6 + docs/payload-migration/environment-parity.md | 2 + docs/payload-migration/host-runtime-plan.md | 235 ++++++++++++++++-- docs/payload-migration/implementation-plan.md | 16 +- 4 files changed, 238 insertions(+), 21 deletions(-) diff --git a/docs/payload-migration/architecture-proposal.md b/docs/payload-migration/architecture-proposal.md index bd68017..7b7f548 100644 --- a/docs/payload-migration/architecture-proposal.md +++ b/docs/payload-migration/architecture-proposal.md @@ -139,8 +139,14 @@ The repository's separate curated file library (`app/files`, 282 files and appro CI builds the frontend, Payload, BGBye, and proxy images once from reviewed commits under their pinned runtimes. Integration tests run those exact digests. Staging and production consume the same core Compose definitions and immutable image digests; they do not rebuild from source or use separately maintained service definitions. Environment overlays may change secrets, hostnames, resource policies, external storage/database endpoints, replica counts, and production TLS integration, but not application image contents or service contracts. +The permanent production checkout and deployment root is `/var/www/lasereverything.net.db`. It contains the complete versioned stack: frontend, Payload, BGBye, Compose definitions, migrations, deployment scripts, internal gateway contract, health checks, and validation tooling. No production checkout or application deployment may run from `/srv`, `/opt`, a Codex workspace, a temporary worktree, or another project root. Operational Docker layers, declared database/media volumes, and server-wide proxy files may live elsewhere, but the deployment rooted at the canonical production checkout declares and controls their contracts. + +The sole persistent nonproduction project root is `/srv/le-payload-dev`, with the checkout at `/srv/le-payload-dev/lasereverything.net.db`. The dedicated account design does not retain the current nested `.worktrees/payload-migration` layout. Temporary worktrees may be created under the canonical nonproduction root for a specific documented task and must be removed when that task ends. + Production database schema changes use committed Payload migrations as an explicit release job before application rollout. Imports are migration-only jobs and are not part of ordinary application startup. Promotion requires recorded image digests, migration status, validation results, rollback compatibility, and health checks. +Promotion never copies the nonproduction checkout or filesystem into production. A reviewed commit/release manifest is fetched into a clean checkout or unpacked as a verified release artifact at `/var/www/lasereverything.net.db`; production then pulls the already-tested immutable image digests. Development caches, local secrets, scrubbed snapshots, reports, test artifacts, rootless state, and disposable volumes are never promotion inputs. + ## Directus metadata disposition See `metadata-disposition.md` for the explicit include, translate, archive-only, and exclude decisions. Exclusion means “not imported into Payload runtime tables,” never “destroyed from the frozen snapshot or archive.” diff --git a/docs/payload-migration/environment-parity.md b/docs/payload-migration/environment-parity.md index 1f4ad74..f049f18 100644 --- a/docs/payload-migration/environment-parity.md +++ b/docs/payload-migration/environment-parity.md @@ -239,6 +239,8 @@ Build verification inspects image histories and filesystems for forbidden names The current `app`, `bgbye`, and `dashboard` directories will be moved only in a dedicated mechanical commit after build-context and path references are covered by tests. Milestone 0 may initially implement the target logical boundaries without a risky all-at-once directory move. +The repository tree above exists at exactly one persistent nonproduction checkout, `/srv/le-payload-dev/lasereverything.net.db`, and at the canonical production root, `/var/www/lasereverything.net.db`. Runtime data is never placed inside either Git checkout unless it is a deliberately versioned non-secret fixture. The complete canonical host directory policy and transitional consolidation are defined in `host-runtime-plan.md`. + ## End-to-end test matrix | Area | Fixture mode | Snapshot/rehearsal mode | Core assertions | diff --git a/docs/payload-migration/host-runtime-plan.md b/docs/payload-migration/host-runtime-plan.md index 9e680a3..875aa1a 100644 --- a/docs/payload-migration/host-runtime-plan.md +++ b/docs/payload-migration/host-runtime-plan.md @@ -39,13 +39,39 @@ Proposed fixed identity: - shell/home: `/bin/bash`, `/home/le_payload_dev`; - no supplementary production groups, no `docker` group, no sudo/wheel membership; - subordinate UID and GID range: `165536:65536`, non-overlapping with the observed `sol6_ii:100000:65536` allocation; -- dedicated project root: Btrfs subvolume `/srv/le-payload-dev`, owner `1200:1200`, mode `0700`; -- workspace: `/srv/le-payload-dev/workspace/lasereverything.net.db`; +- dedicated project root: Btrfs subvolume `/srv/le-payload-dev`, owner `root:le_payload_dev`, mode `0750`; +- sole checkout: `/srv/le-payload-dev/lasereverything.net.db`; +- authoritative scrubbed input: `/srv/le-payload-dev/source`, root-owned and read-only to the development group; +- nonproduction credentials only: `/srv/le-payload-dev/secrets`; - daemon data root: `/srv/le-payload-dev/runtime/docker`; - runtime socket: `/run/user/1200/docker.sock`; - Docker client/config: `/home/le_payload_dev/.docker` and `/home/le_payload_dev/.config/docker`; - Compose project prefix: `le_payload_dev`, enforced by wrapper preflight. +Canonical nonproduction tree: + +```text +/srv/le-payload-dev/ +├── lasereverything.net.db/ # sole persistent nonproduction Git checkout +├── source/ # root-owned scrubbed authoritative input +├── secrets/ # nonproduction credentials only +├── runtime/ # rootless Docker data/configuration support +├── data/ # disposable PostgreSQL, media, clones, caches, temp +└── artifacts/ # migration reports, logs, browser evidence +``` + +Ownership and permissions: + +| Path | Owner:group | Mode/policy | +|---|---|---| +| `/srv/le-payload-dev` | `root:le_payload_dev` | `0750`; prevents unrelated users and prevents project-root restructuring by the development account | +| `lasereverything.net.db` | `le_payload_dev:le_payload_dev` | directories `0750`, normal Git files subject to repository modes; sole checkout | +| `source` | `root:le_payload_dev` | directories `0550`, files `0440`; account and daemon may read but cannot alter; no writable symlink target | +| `secrets` | `root:le_payload_dev` | directory `0750`, provisioned files `0640`; nonproduction values only; optional `generated/` is `0700 le_payload_dev` for ephemeral fixture secrets | +| `runtime` | `le_payload_dev:le_payload_dev` | `0700`; rootless Docker layers/configuration only | +| `data` | `le_payload_dev:le_payload_dev` | `0700`; child ownership may use rootless subordinate IDs | +| `artifacts` | `le_payload_dev:le_payload_dev` | `0700`; TTL cleanup; snapshot artifacts treated as sensitive | + Only the dedicated account receives: - read/write access to its workspace and runtime/storage subtree; @@ -82,23 +108,32 @@ sudo btrfs subvolume create /srv/le-payload-dev sudo chown 1200:1200 /srv/le-payload-dev sudo chmod 0700 /srv/le-payload-dev sudo -u le_payload_dev mkdir -p \ - /srv/le-payload-dev/workspace \ + /srv/le-payload-dev/lasereverything.net.db \ + /srv/le-payload-dev/source \ + /srv/le-payload-dev/secrets/generated \ /srv/le-payload-dev/runtime/docker \ /srv/le-payload-dev/data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ - /srv/le-payload-dev/artifacts/{browser,migration-reports} \ - /srv/le-payload-dev/secrets + /srv/le-payload-dev/artifacts/{browser,migration-reports} + +# Root owns the boundary and authoritative source; development owns mutable areas. +sudo chown root:le_payload_dev /srv/le-payload-dev +sudo chmod 0750 /srv/le-payload-dev +sudo chown -R le_payload_dev:le_payload_dev \ + /srv/le-payload-dev/lasereverything.net.db \ + /srv/le-payload-dev/runtime /srv/le-payload-dev/data /srv/le-payload-dev/artifacts \ + /srv/le-payload-dev/secrets/generated +sudo chmod 0700 /srv/le-payload-dev/runtime /srv/le-payload-dev/data \ + /srv/le-payload-dev/artifacts /srv/le-payload-dev/secrets/generated +sudo chown root:le_payload_dev /srv/le-payload-dev/source /srv/le-payload-dev/secrets +sudo chmod 0550 /srv/le-payload-dev/source +sudo chmod 0750 /srv/le-payload-dev/secrets # 5. Apply a 250 GiB ceiling to the project subvolume. sudo btrfs quota enable /srv sudo btrfs qgroup limit 250G /srv/le-payload-dev -# 6. Grant only source traversal/read and one migration secret file. -# Recheck the resolved `current` target before applying ACLs. -readlink -f /srv/codex-migration/current -sudo setfacl -m u:le_payload_dev:--x /srv/codex-migration -sudo setfacl -R -m u:le_payload_dev:r-X /srv/codex-migration/source-20260710-175408 -sudo setfacl -m u:le_payload_dev:--x /srv/codex-secrets -sudo setfacl -m u:le_payload_dev:r-- /srv/codex-secrets/le-payload-migration.env +# 6. Transitional source access is used only by the administrator-run copy and +# verification procedure below. The final account needs no ACL on /srv/codex-*. # 7. Remediate production secret readability separately. # Root-owned Compose can still read mode 0600 files. Confirm the production @@ -141,10 +176,10 @@ test ! -x /var/lib/docker test ! -x /var/lib/mysql test ! -r /var/run/docker.sock test ! -x /srv/directus-archive -test -r /srv/codex-migration/source-20260710-175408/README.txt -test ! -w /srv/codex-migration/source-20260710-175408/README.txt -test -r /srv/codex-secrets/le-payload-migration.env -test ! -w /srv/codex-secrets/le-payload-migration.env +test -r /srv/le-payload-dev/source/README.txt +test ! -w /srv/le-payload-dev/source/README.txt +test -r /srv/le-payload-dev/secrets/migration.env +test ! -w /srv/le-payload-dev/secrets/migration.env DOCKER_HOST=unix:///run/user/1200/docker.sock docker info --format '{{json .SecurityOptions}}' ``` @@ -158,6 +193,7 @@ Every stack/job wrapper will fail before Compose evaluation if: - Docker endpoint is not exactly the approved rootless Unix socket; - Compose project name lacks the fixed nonproduction prefix; - any resolved bind source is `/var/www`, `/var/lib/docker`, `/var/lib/mysql`, `/srv/directus-archive`, `/var/run`, `/run/docker.sock`, or a descendant; +- the Git checkout is not exactly `/srv/le-payload-dev/lasereverything.net.db` in nonproduction or `/var/www/lasereverything.net.db` in production; - any non-finite application service receives `.migration.env`, `.migration-source`, a MariaDB source URL, or migration credentials; - destination database host/port is not the approved nonproduction target/service; - production-named secret scopes, SMTP hosts, OAuth variables, Ko-fi secrets, or webhook credentials appear in fixture/snapshot/rehearsal rendered Compose; @@ -175,6 +211,8 @@ Tests render each Compose mode and inspect mounts, secrets, environment variable - Reset/purge operates only on labeled nonproduction destinations. It does not follow source symlinks and refuses all external/bind volumes. - `/srv/directus-archive` remains inaccessible and is never mentioned as a mount source in executable Compose configuration. +The complete root-only archive remains permanently outside the development boundary at `/srv/directus-archive/20260710-175408`. It is never moved, mounted, linked, or copied into `/srv/le-payload-dev`. + ## Nonproduction storage plan The host currently has approximately 650 GiB available on the Btrfs filesystem containing `/srv` and `/home`. Proposed aggregate project quota: 250 GiB. @@ -193,6 +231,19 @@ The host currently has approximately 650 GiB available on the Btrfs filesystem c The listed soft budgets total 200 GiB. The 250 GiB hard qgroup ceiling reserves 50 GiB emergency headroom. Startup refuses when host free space is below 100 GiB, project usage exceeds 200 GiB, or required free space for a rehearsal cannot be reserved. The production filesystem is not used for spillover. +## Resources intentionally outside `/srv/le-payload-dev` + +| Resource | Permanent location | Why it remains outside | +|---|---|---| +| Complete untouched Directus archive | `/srv/directus-archive/20260710-175408` | Root-only disaster-recovery evidence; must not enter the Codex trust boundary | +| Production checkout/deployment root | `/var/www/lasereverything.net.db` | Canonical production policy and existing shared-server operations boundary | +| Production Docker image/layer storage | Host runtime-managed location | Operational daemon state, declared by the canonical production deployment but not Git content | +| Production PostgreSQL/media storage | Approved production volume/storage paths | Persistent production data requires independent ownership, capacity, and backup controls | +| Shared public reverse-proxy configuration/certificates | Existing TITANSERVER ingress paths | Server-wide infrastructure serves multiple applications and is never container-controlled | +| Existing self-hosted SMTP infrastructure | Existing mail-server deployment | Production email remains independent; Mailpit is nonproduction-only | + +Transitional `/srv/codex-*` paths remain temporarily outside during the rollback period only. They are not permanent alternate project roots and become cleanup candidates solely through the explicit process below. + ## Production ingress boundary Production will use an **internal application-gateway container behind TITANSERVER's existing shared host reverse proxy**. @@ -206,6 +257,156 @@ Production will use an **internal application-gateway container behind TITANSERV Local development uses its own gateway/TLS because it does not share production ingress. This design preserves server-wide ownership while keeping application routing rules testable and versioned. +## Transitional resource inventory + +Metadata-only audit results at the planning checkpoint: + +| Resource | Current state | +|---|---| +| `/srv/codex-work/lasereverything.net.db` | Primary Git checkout, branch `temporary/utilities-only`, commit `305ea41`; owns the current local Git database | +| nested `.worktrees/payload-migration` | Migration worktree, branch `migration/payload`; planning commit begins at `ed1c84e` | +| `.migration-source` | Symlink to `/srv/codex-migration/current` | +| `.migration.env` | Symlink to `/srv/codex-secrets/le-payload-migration.env` | +| `/srv/codex-migration/current` | Symlink to `source-20260710-175408` | +| `/srv/codex-migration/source-20260710-175408` | Scrubbed source snapshot, mode `0750`, approximately 150 MB aggregate at audit time | +| `/srv/codex-secrets/le-payload-migration.env` | Current readable nonproduction migration environment, mode `0640`; values not inspected | +| `/srv/codex-secrets/le-payload-migration-admin.env` | Root-only transitional administration environment, mode `0600`; values not inspected and not copied into the development environment | +| `/srv/codex-work` | Approximately 2.0 GB aggregate, including repository history/worktrees and application files | +| recorded rootful MariaDB | container `le-directus-source`, volume `le_directus_source_data_20260710-175408` | +| recorded rootful PostgreSQL | container `le-payload-pg`, volume `le_payload_pg_data_20260710-175408` | +| retired Directus | container `directus`; not a disposable migration resource and not removed by consolidation | + +The current user cannot query the rootful production Docker daemon. Before consolidation, an administrator must produce a metadata-only inventory without printing container environments, labels that contain secrets, database contents, or mount-file contents: + +```bash +sudo docker ps -a --filter name=^/le-directus-source$ \ + --filter name=^/le-payload-pg$ --format '{{.Names}} {{.Image}} {{.Status}}' +sudo docker inspect le-directus-source le-payload-pg \ + --format '{{.Name}} image={{.Image}} mounts={{range .Mounts}}{{.Type}}:{{.Name}}:{{.Destination}};{{end}}' +sudo docker volume inspect \ + le_directus_source_data_20260710-175408 \ + le_payload_pg_data_20260710-175408 \ + --format '{{.Name}} driver={{.Driver}} mountpoint={{.Mountpoint}} labels={{json .Labels}}' +``` + +If names, image IDs, volumes, or mounts differ from the recorded manifest, stop and revise the plan. Do not inspect `.Config.Env` or print secret values. + +## Reversible consolidation procedure + +No step deletes or mutates a transitional resource. Consolidation is copy/recreate/verify, followed by a rollback observation period. + +### Phase A — manifests and Git preservation + +1. Push `migration/payload`, including planning commits, to `origin` and verify the remote commit. +2. Record structural manifests for every transitional path, symlink, owner/mode, aggregate size, Git branch/commit/remote, source checksum manifest, and the administrator container/volume inventory above. +3. Confirm the full root-only archive remains independently present and inaccessible; do not traverse or copy it. +4. Record the exact existing rootful container/volume names and leave them stopped/running exactly as found unless a later approved step says otherwise. + +### Phase B — canonical checkout + +After account/runtime approval, create one clean checkout as the dedicated user: + +```bash +sudo -iu le_payload_dev git clone --branch migration/payload --single-branch \ + ssh://FORGE/makearmy/le-app.git \ + /srv/le-payload-dev/lasereverything.net.db +sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ + fetch --prune origin +sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ + status --short --branch +sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ + rev-parse HEAD +``` + +The HEAD must equal the reviewed remote `migration/payload` commit, the worktree must be clean, and no nested permanent worktree may exist. Provision a dedicated repository-scoped Git credential for `le_payload_dev`; do not copy `sol6_vi`'s private key. + +### Phase C — scrubbed source copy + +The administrator copies from the resolved source directory, never from the root-only archive: + +```bash +source_old=/srv/codex-migration/source-20260710-175408 +source_new=/srv/le-payload-dev/source + +sudo rsync -aHAX --numeric-ids --delete-delay "$source_old/" "$source_new/" + +# Verify supplied manifests without emitting filenames or record contents. +sudo sh -c "cd '$source_new' && sha256sum --check --status CHECKSUMS.sha256" +sudo sh -c "cd '$source_new' && sha256sum --check --status uploads.sha256" + +# Freeze the canonical input against development writes. +sudo chown -R root:le_payload_dev "$source_new" +sudo find "$source_new" -type d -exec chmod 0550 {} + +sudo find "$source_new" -type f -exec chmod 0440 {} + +``` + +Before using `--delete-delay`, assert `source_new` resolves exactly beneath `/srv/le-payload-dev/source` and is not a symlink. The source checksum files, row-count manifests, schema, dump, extensions, and all uploads must validate. The old copy remains untouched. + +### Phase D — nonproduction secrets + +- Preserve both old credential files in place during rollback. +- Do not copy `le-payload-migration-admin.env`. +- Generate new fixture/snapshot/rootless database, Payload, Mailpit, and local gateway credentials directly into root-owned `/srv/le-payload-dev/secrets` with mode `0640`. +- If the MariaDB read-only credential from the scrubbed environment is temporarily required to compare the old rootful source, transfer only the minimum required variable set through an administrator-reviewed, non-logging process; replace it once the rootless source clone is verified. +- No production SMTP, OAuth, Ko-fi, Directus token, webhook, or server credential enters the new directory. + +### Phase E — rootless database recreation + +1. Restore the scrubbed SQL from canonical read-only `source` into a new project-scoped rootless MariaDB volume. +2. Create a distinct import account with `SELECT` only; application/migration jobs never receive the MariaDB administrative credential. +3. Mount/copy uploads only through finite jobs; the MariaDB and legacy Directus services never receive writable access to canonical `source`. +4. Create fresh disposable rootless PostgreSQL and Payload media volumes. +5. Verify source table counts/checksum summaries against supplied manifests and verify PostgreSQL health/reset isolation. +6. Record new rootless image digests, volume names, Compose project labels, and service health without recording secrets. + +### Phase F — rollback observation period + +Keep `/srv/codex-work`, `/srv/codex-migration`, `/srv/codex-secrets`, rootful `le-directus-source`, rootful `le-payload-pg`, and their recorded volumes intact for at least 30 days **and** through at least two complete successful rootless migration rehearsals, whichever is later. During this period: + +- new work occurs only in the canonical checkout; +- old resources are read-only/frozen where possible; +- checksum, Git, service, and validation comparisons remain reproducible; +- rollback means stop the rootless stack and resume analysis from the untouched transitional resources, not copy changes backward. + +## Later cleanup eligibility — explicit approval required + +Only after the rollback criteria and a fresh inventory may the following become eligible for removal: + +- `/srv/codex-work/lasereverything.net.db/.worktrees/payload-migration` after Git confirms it is clean, pushed, and removable with `git worktree remove`; +- `/srv/codex-work/lasereverything.net.db/.migration-source` and `.migration.env` symlinks; +- `/srv/codex-work/lasereverything.net.db`, then `/srv/codex-work` only if no other entries exist; +- `/srv/codex-migration/current`, `source-20260710-175408`, then `/srv/codex-migration` after canonical checksums pass and rollback expires; +- `/srv/codex-secrets/le-payload-migration.env` and `le-payload-migration-admin.env`, then `/srv/codex-secrets`, after credentials are revoked/obsolete and rollback expires; +- rootful containers `le-directus-source` and `le-payload-pg`; +- rootful volumes `le_directus_source_data_20260710-175408` and `le_payload_pg_data_20260710-175408` after verifying their exact attachment and backup status. + +Every deletion requires a separate explicit approval showing the resolved path/object, before/after checksum or Git proof, attachment status, and rollback consequence. Never include the retired `directus` container, production volumes, `/var/www`, or `/srv/directus-archive/20260710-175408` in this cleanup set. + +## Canonical production promotion + +Promotion path: + +```text +/srv/le-payload-dev/lasereverything.net.db + -> reviewed Git commit + signed/recorded release manifest + -> CI-built immutable image digests tested in fixture/snapshot/rehearsal/staging + -> clean checkout or verified release artifact at /var/www/lasereverything.net.db + -> production deployment invoked only from /var/www/lasereverything.net.db +``` + +Required workflow: + +1. The canonical nonproduction checkout is clean and all accepted commits are pushed. +2. CI builds images once, records digests/SBOM/provenance, and runs the accepted test gates. +3. Staging deploys those exact digests from the same Compose core and an approved staging overlay. +4. A reviewed release manifest pins Git commit, image digests, migration set, configuration schema, health contract, and rollback compatibility. +5. On TITANSERVER, an operator updates a clean Git checkout at `/var/www/lasereverything.net.db` to the reviewed commit (or atomically unpacks a verified release artifact into that exact root), verifies a clean tree, and verifies the release manifest. +6. From that directory only, deployment scripts render/validate production Compose and the host-ingress include, run the gated schema job, pull pinned digests, and roll services with health checks. + +The production checkout contains all application source/configuration needed to reproduce the deployment contract. Physical image layers, declared production PostgreSQL/media volumes, and shared host-proxy configuration may remain outside it, but their names/locations/interfaces are declared in the production overlay and release manifest rooted there. + +Forbidden promotion inputs include `/srv/le-payload-dev/runtime`, `source`, `secrets`, `data`, `artifacts`, local CA material, Mailpit state, test traces, rootless Docker state, disposable databases/volumes, caches, and development `.env` files. No `rsync` or recursive copy from `/srv/le-payload-dev` to `/var/www` is permitted. + ## Local HTTPS and CA trust The local gateway uses a repository-pinned proxy image and an ignored, generated development CA to serve: @@ -286,6 +487,6 @@ sudo groupdel le_payload_dev 2>/dev/null || true The dedicated Docker context lives only in the dedicated user's home and disappears with that home. If another operator created a client context pointing to the user socket, remove that context explicitly from that operator's client configuration. -Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Source ACL entries can be removed with `setfacl -x u:le_payload_dev` before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, the frozen source, or `/srv/directus-archive`. +Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Any temporary transitional ACLs, if an administrator added them contrary to the final copy-based design, must be inventoried and removed before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, transitional source during its rollback retention, or `/srv/directus-archive`. Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-payload-dev` resources were deleted. diff --git a/docs/payload-migration/implementation-plan.md b/docs/payload-migration/implementation-plan.md index 5fb35f9..2170132 100644 --- a/docs/payload-migration/implementation-plan.md +++ b/docs/payload-migration/implementation-plan.md @@ -17,29 +17,37 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt - Verify forbidden production paths and sockets are inaccessible. - Execute every positive and negative preflight in `host-runtime-plan.md` before application work. -3. **Repository/environment structure** +3. **Reversible canonical-path consolidation — separately approval-gated** + - Inventory the transitional Git roots, symlinks, credential filenames, rootful disposable containers, and volumes without reading secret values. + - Create the sole checkout at `/srv/le-payload-dev/lasereverything.net.db` and verify `migration/payload` commit/remote state. + - Copy and checksum the scrubbed source into root-owned read-only `/srv/le-payload-dev/source`. + - Create new nonproduction-only secrets and rootless MariaDB/PostgreSQL resources; do not reuse production credentials. + - Preserve all old resources through the rollback period. Delete nothing without a later explicit approval. + +4. **Repository/environment structure** - Add common Compose core, mode overlays/profiles, environment schemas/examples, proxy configuration, health contracts, and safe defaults. - Complete Docker/build-context exclusions for every context. - Add one-command wrappers with context/path/project safety checks. - Add automated external-SMTP denial and forbidden-mount/context tests; make snapshot startup fail closed. -4. **Fixture stack** +5. **Fixture stack** - Build exact pinned images under Node 24.18.0. - Bring up proxy, frontend, Payload shell, PostgreSQL, Mailpit, deterministic CPU BGBye profile, and fixtures. - Prove HTTPS cookies, forwarded headers, uploads, mail capture, health, logs, reset, and teardown. -5. **Snapshot/rehearsal stack** +6. **Snapshot/rehearsal stack** - Mount only authorized scrubbed source and uploads read-only on finite jobs. - Add disposable destination media/PostgreSQL and detailed-report exclusions. - Add isolated legacy Directus reference profile using a writable clone and local-only credentials. - Prove no production SMTP/OAuth/webhook/token configuration or outbound delivery. - Keep authoritative source and `.migration.env` mounted only on finite inventory/import/validation jobs. -6. **Milestone 0 acceptance** +7. **Milestone 0 acceptance** - Clean-checkout fixture startup and smoke test require no undocumented steps. - Snapshot startup passes mount/secret/egress safety assertions. - Same image digests run in fixture and snapshot modes. - Rootless user can control only its nonproduction project resources. + - Production release dry-run proves a clean reviewed commit and immutable digests deploy only from `/var/www/lasereverything.net.db`. ## Milestone 1 — structural inventory and mapping From 0e3d5ea5ca29dd84a9101c9024327c79881b7ba1 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 10 Jul 2026 20:38:40 -0400 Subject: [PATCH 05/14] standardize laser everything migration names --- .../architecture-proposal.md | 6 +- .../environment-parity.md | 10 +- .../host-runtime-plan.md | 295 +++++++++++------- .../implementation-plan.md | 8 +- .../metadata-disposition.md | 0 5 files changed, 197 insertions(+), 122 deletions(-) rename docs/{payload-migration => le-app-database-migration}/architecture-proposal.md (94%) rename docs/{payload-migration => le-app-database-migration}/environment-parity.md (96%) rename docs/{payload-migration => le-app-database-migration}/host-runtime-plan.md (64%) rename docs/{payload-migration => le-app-database-migration}/implementation-plan.md (94%) rename docs/{payload-migration => le-app-database-migration}/metadata-disposition.md (100%) diff --git a/docs/payload-migration/architecture-proposal.md b/docs/le-app-database-migration/architecture-proposal.md similarity index 94% rename from docs/payload-migration/architecture-proposal.md rename to docs/le-app-database-migration/architecture-proposal.md index 7b7f548..e48cf76 100644 --- a/docs/payload-migration/architecture-proposal.md +++ b/docs/le-app-database-migration/architecture-proposal.md @@ -1,4 +1,4 @@ -# Payload migration architecture proposal +# Laser Everything database transition architecture Status: pre-implementation decision record. No production changes are authorized by this document. @@ -31,7 +31,7 @@ The migration branch initially started from `main` at `0a7ee5f`. A tree and hist - Those commits repair production registration/authentication behavior and add the verified Directus deployment contract, policies, flows, exports, and schema snapshot. - The application was not replaced or reduced in those commits. -Conclusion: `main` is a complete application tree, but `4884b5d` is the correct and more complete pre-utilities production baseline. The dedicated `migration/payload` branch has therefore been fast-forwarded to `4884b5d`. The temporary utilities branch is not an ancestor of the migration work. +Conclusion: `main` is a complete application tree, but `4884b5d` is the correct and more complete pre-utilities production baseline. The dedicated `migration/le-app-database` branch has therefore been fast-forwarded to `4884b5d`. The temporary utilities branch is not an ancestor of the migration work. ## Service boundary @@ -141,7 +141,7 @@ CI builds the frontend, Payload, BGBye, and proxy images once from reviewed comm The permanent production checkout and deployment root is `/var/www/lasereverything.net.db`. It contains the complete versioned stack: frontend, Payload, BGBye, Compose definitions, migrations, deployment scripts, internal gateway contract, health checks, and validation tooling. No production checkout or application deployment may run from `/srv`, `/opt`, a Codex workspace, a temporary worktree, or another project root. Operational Docker layers, declared database/media volumes, and server-wide proxy files may live elsewhere, but the deployment rooted at the canonical production checkout declares and controls their contracts. -The sole persistent nonproduction project root is `/srv/le-payload-dev`, with the checkout at `/srv/le-payload-dev/lasereverything.net.db`. The dedicated account design does not retain the current nested `.worktrees/payload-migration` layout. Temporary worktrees may be created under the canonical nonproduction root for a specific documented task and must be removed when that task ends. +The sole persistent nonproduction project root is `/srv/le-app-codex`, with the checkout at `/srv/le-app-codex/lasereverything.net.db`. The dedicated account design does not retain the current nested `.worktrees/payload-migration` layout. Temporary worktrees may be created under the canonical nonproduction root for a specific documented task and must be removed when that task ends. Production database schema changes use committed Payload migrations as an explicit release job before application rollout. Imports are migration-only jobs and are not part of ordinary application startup. Promotion requires recorded image digests, migration status, validation results, rollback compatibility, and health checks. diff --git a/docs/payload-migration/environment-parity.md b/docs/le-app-database-migration/environment-parity.md similarity index 96% rename from docs/payload-migration/environment-parity.md rename to docs/le-app-database-migration/environment-parity.md index f049f18..68ad642 100644 --- a/docs/payload-migration/environment-parity.md +++ b/docs/le-app-database-migration/environment-parity.md @@ -140,7 +140,7 @@ Local HTTPS is required to test secure cookies. A repository-controlled developm ## Rootless Codex-controlled runtime -Required target: a dedicated rootless Docker daemon owned by the separate `le_payload_dev` account. Future Codex migration sessions must execute as that account (preferred) or through a narrowly scoped account transition; the daemon socket is not shared broadly. Rootless Podman is an acceptable fallback only after Compose/profile behavior is proven; Docker is preferred because the repository and production already use Compose. +Required target: a dedicated rootless Docker daemon owned by the separate `le_app_codex` account. Future Codex migration sessions must execute as that account (preferred) or through a narrowly scoped account transition; the daemon socket is not shared broadly. Rootless Podman is an acceptable fallback only after Compose/profile behavior is proven; Docker is preferred because the repository and production already use Compose. Codex must be able to build images, create/remove only its project containers/networks/volumes, inspect their logs, reset disposable PostgreSQL, run finite jobs, and execute integration/browser tests. It must not access `/var/run/docker.sock`, the production daemon/context, production containers/volumes/networks, `/var/www`, or `/srv/directus-archive`. @@ -154,9 +154,9 @@ Current host observation: Therefore a `sol6_vi`-owned daemon is rejected: it could bind-mount those readable production secrets. Milestone 0 requires the isolated account and host changes described in `host-runtime-plan.md`, and work must stop before performing them. An administrator must approve/provision: -1. dedicated `le_payload_dev` identity and restricted filesystem ACLs; +1. dedicated `le_app_codex` identity and restricted filesystem ACLs; 2. rootless prerequisites (`newuidmap`/`newgidmap`, subordinate UID/GID ranges, rootless networking and overlay helpers as required by the OS); -3. rootless Docker daemon installation for `le_payload_dev` using its dedicated data root and named context; +3. rootless Docker daemon installation for `le_app_codex` using its dedicated data root and named context; 4. optional user-service lingering if stacks must survive logout; 5. deterministic CPU BGBye first; optional GPU access only after core acceptance; 6. user/browser-profile-scoped development CA trust; @@ -231,7 +231,7 @@ Build verification inspects image histories and filesystems for forbidden names │ ├── contracts/ │ ├── e2e/ │ └── fixtures/files/ -├── docs/payload-migration/ +├── docs/le-app-database-migration/ ├── directus/ # retained legacy evidence during migration ├── .nvmrc / .node-version └── Dockerfiles remain beside each build context @@ -239,7 +239,7 @@ Build verification inspects image histories and filesystems for forbidden names The current `app`, `bgbye`, and `dashboard` directories will be moved only in a dedicated mechanical commit after build-context and path references are covered by tests. Milestone 0 may initially implement the target logical boundaries without a risky all-at-once directory move. -The repository tree above exists at exactly one persistent nonproduction checkout, `/srv/le-payload-dev/lasereverything.net.db`, and at the canonical production root, `/var/www/lasereverything.net.db`. Runtime data is never placed inside either Git checkout unless it is a deliberately versioned non-secret fixture. The complete canonical host directory policy and transitional consolidation are defined in `host-runtime-plan.md`. +The repository tree above exists at exactly one persistent nonproduction checkout, `/srv/le-app-codex/lasereverything.net.db`, and at the canonical production root, `/var/www/lasereverything.net.db`. Runtime data is never placed inside either Git checkout unless it is a deliberately versioned non-secret fixture. The complete canonical host directory policy and transitional consolidation are defined in `host-runtime-plan.md`. ## End-to-end test matrix diff --git a/docs/payload-migration/host-runtime-plan.md b/docs/le-app-database-migration/host-runtime-plan.md similarity index 64% rename from docs/payload-migration/host-runtime-plan.md rename to docs/le-app-database-migration/host-runtime-plan.md index 875aa1a..2482f48 100644 --- a/docs/payload-migration/host-runtime-plan.md +++ b/docs/le-app-database-migration/host-runtime-plan.md @@ -26,57 +26,90 @@ Conclusion: rootless Docker owned by `sol6_vi` would protect the production daem | Model | Advantages | Risks / cost | Decision | |---|---|---|---| | `sol6_vi` owns rootless Docker | Minimal provisioning; current workspace already writable | Can bind-mount world-readable production secrets and application files; mixes general Codex work with container authority | Rejected on this host | -| dedicated `le_payload_dev` | Daemon owner has an explicit filesystem allow-list; project storage and cleanup are attributable; production paths can be proven inaccessible | Requires account, ACL, subordinate IDs, separate Git credentials/session, storage, and user service | **Recommended** | +| dedicated `le_app_codex` | Daemon owner has an explicit filesystem allow-list; project storage and cleanup are attributable; production paths can be proven inaccessible | Requires account, ACL, subordinate IDs, separate Git credentials/session, storage, and user service | **Recommended** | -Future autonomous migration work should run in a Codex session whose OS identity is `le_payload_dev`. Do not grant `sol6_vi` generic access to the development daemon socket as a convenience; that weakens accountability. If orchestration must be initiated from another account, use a narrowly audited command wrapper or SSH into `le_payload_dev`, not a shared Docker group and not TCP exposure of the daemon. +Future autonomous migration work should run in a Codex session whose OS identity is `le_app_codex`. Do not grant `sol6_vi` generic access to the development daemon socket as a convenience; that weakens accountability. If orchestration must be initiated from another account, use a narrowly audited command wrapper or SSH into `le_app_codex`, not a shared Docker group and not TCP exposure of the daemon. ## Exact proposed identity and filesystem model Proposed fixed identity: -- user and primary group: `le_payload_dev`; +- user and primary group: `le_app_codex`; - UID/GID: `1200` (verified unused at audit time; recheck immediately before creation); -- shell/home: `/bin/bash`, `/home/le_payload_dev`; +- shell/home: `/bin/bash`, `/srv/le-app-codex/home`; - no supplementary production groups, no `docker` group, no sudo/wheel membership; - subordinate UID and GID range: `165536:65536`, non-overlapping with the observed `sol6_ii:100000:65536` allocation; -- dedicated project root: Btrfs subvolume `/srv/le-payload-dev`, owner `root:le_payload_dev`, mode `0750`; -- sole checkout: `/srv/le-payload-dev/lasereverything.net.db`; -- authoritative scrubbed input: `/srv/le-payload-dev/source`, root-owned and read-only to the development group; -- nonproduction credentials only: `/srv/le-payload-dev/secrets`; -- daemon data root: `/srv/le-payload-dev/runtime/docker`; +- dedicated project root: Btrfs subvolume `/srv/le-app-codex`, owner `root:le_app_codex`, mode `0750`; +- sole checkout: `/srv/le-app-codex/lasereverything.net.db`; +- authoritative scrubbed input: `/srv/le-app-codex/database-source-readonly`, root-owned and read-only to the development group; +- nonproduction credentials only: `/srv/le-app-codex/nonproduction-secrets`; +- daemon data root: `/srv/le-app-codex/container-runtime/docker`; - runtime socket: `/run/user/1200/docker.sock`; -- Docker client/config: `/home/le_payload_dev/.docker` and `/home/le_payload_dev/.config/docker`; -- Compose project prefix: `le_payload_dev`, enforced by wrapper preflight. +- Docker client/config: `/srv/le-app-codex/home/.docker` and `/srv/le-app-codex/home/.config/docker`; +- rootless Docker context: `le-app-codex`; +- default nonproduction Compose project: `le-app-testing`, enforced by wrapper preflight. + +Verified candidate availability at this planning checkpoint: + +- neither account nor group name `le_app_codex` exists; +- UID `1200` is unused in `/etc/passwd` and GID `1200` is unused in `/etc/group`; +- no passwd primary UID/GID or group GID collision exists at `1200`; +- `/etc/subuid` and `/etc/subgid` contain one observed allocation, `100000–165535`, owned by the historical `sol6_ii` entry; +- candidate range `165536–231071` does not overlap any entry in either subordinate-ID file; +- `/srv/le-app-codex` does not exist. + +Therefore the current exact recommendation is UID/GID `1200` and subordinate range `165536:65536`, subject to rerunning the documented collision checks immediately before provisioning. If any check produces output or a non-free result, stop and select new IDs; do not force reuse. + +## Canonical naming replacements + +| Superseded proposal | Canonical replacement | +|---|---| +| account/group `le_payload_dev` | `le_app_codex` | +| nonproduction root `/srv/le-payload-dev` | `/srv/le-app-codex` | +| checkout `/srv/le-payload-dev/lasereverything.net.db` | `/srv/le-app-codex/lasereverything.net.db` | +| source directory `source` | `database-source-readonly` | +| secrets directory `secrets` | `nonproduction-secrets` | +| runtime directory `runtime` | `container-runtime` | +| disposable-data directory `data` | `testing-data` | +| artifact directory `artifacts` | `build-artifacts` | +| Docker context `le-payload-dev` | `le-app-codex` | +| Compose project `le_payload_dev` | `le-app-testing` | +| branch `migration/payload` | `migration/le-app-database` | +| documentation `docs/payload-migration` | `docs/le-app-database-migration` | + +`payload` remains valid only for the actual Payload CMS service, application directory, packages, configuration, and Payload-specific migrations. Historical inventory names such as `le-payload-pg` and `.worktrees/payload-migration` remain unchanged because they identify real transitional resources. Canonical nonproduction tree: ```text -/srv/le-payload-dev/ -├── lasereverything.net.db/ # sole persistent nonproduction Git checkout -├── source/ # root-owned scrubbed authoritative input -├── secrets/ # nonproduction credentials only -├── runtime/ # rootless Docker data/configuration support -├── data/ # disposable PostgreSQL, media, clones, caches, temp -└── artifacts/ # migration reports, logs, browser evidence +/srv/le-app-codex/ +├── home/ # dedicated account home and user configuration +├── lasereverything.net.db/ # sole persistent nonproduction Git checkout +├── database-source-readonly/ # root-owned scrubbed authoritative input +├── nonproduction-secrets/ # testing/development credentials only +├── container-runtime/ # rootless Docker images, layers and configuration +├── testing-data/ # disposable databases, media, models and caches +└── build-artifacts/ # reports, logs and browser/test evidence ``` Ownership and permissions: | Path | Owner:group | Mode/policy | |---|---|---| -| `/srv/le-payload-dev` | `root:le_payload_dev` | `0750`; prevents unrelated users and prevents project-root restructuring by the development account | -| `lasereverything.net.db` | `le_payload_dev:le_payload_dev` | directories `0750`, normal Git files subject to repository modes; sole checkout | -| `source` | `root:le_payload_dev` | directories `0550`, files `0440`; account and daemon may read but cannot alter; no writable symlink target | -| `secrets` | `root:le_payload_dev` | directory `0750`, provisioned files `0640`; nonproduction values only; optional `generated/` is `0700 le_payload_dev` for ephemeral fixture secrets | -| `runtime` | `le_payload_dev:le_payload_dev` | `0700`; rootless Docker layers/configuration only | -| `data` | `le_payload_dev:le_payload_dev` | `0700`; child ownership may use rootless subordinate IDs | -| `artifacts` | `le_payload_dev:le_payload_dev` | `0700`; TTL cleanup; snapshot artifacts treated as sensitive | +| `/srv/le-app-codex` | `root:le_app_codex` | `0750`; prevents unrelated users and prevents project-root restructuring by the development account | +| `home` | `le_app_codex:le_app_codex` | `0700`; account, Docker client, user systemd, NSS/browser configuration | +| `lasereverything.net.db` | `le_app_codex:le_app_codex` | directories `0750`, normal Git files subject to repository modes; sole checkout | +| `database-source-readonly` | `root:le_app_codex` | directories `0550`, files `0440`; account and daemon may read but cannot alter; no writable symlink target | +| `nonproduction-secrets` | `root:le_app_codex` | directory `0750`, provisioned files `0640`; nonproduction values only; optional `generated/` is `0700 le_app_codex` for ephemeral fixture secrets | +| `container-runtime` | `le_app_codex:le_app_codex` | `0700`; rootless Docker images, layers and configuration only | +| `testing-data` | `le_app_codex:le_app_codex` | `0700`; child ownership may use rootless subordinate IDs | +| `build-artifacts` | `le_app_codex:le_app_codex` | `0700`; TTL cleanup; snapshot artifacts treated as sensitive | Only the dedicated account receives: - read/write access to its workspace and runtime/storage subtree; -- read/traverse ACL on the frozen scrubbed source only; -- read ACL on `/srv/codex-secrets/le-payload-migration.env` only; +- group read/traverse access to canonical root-owned `database-source-readonly`; +- group read access to specifically provisioned files under canonical `nonproduction-secrets`; - a separate nonproduction Git credential/deploy key with access limited to the application repository. It receives no ACL or group membership for `/var/www`, production database paths, production sockets/volumes, or `/srv/directus-archive`. @@ -86,10 +119,16 @@ It receives no ACL or group membership for `/var/www`, production database paths These commands are a reviewable proposal for an administrator on Garuda/Arch Linux. Recheck UID/GID and subordinate ranges first. Do not paste secrets into the shell history. ```bash -# 1. Recheck proposed IDs/ranges are unused. +# 1. Recheck names, IDs, and ranges across all four account databases. +getent passwd le_app_codex +getent group le_app_codex getent passwd 1200 getent group 1200 -grep -E '^(le_payload_dev|[^:]+:165536:65536)$' /etc/subuid /etc/subgid +awk -F: '$3 == 1200 || $4 == 1200 { print; found=1 } END { exit found ? 1 : 0 }' /etc/passwd +awk -F: '$3 == 1200 { print; found=1 } END { exit found ? 1 : 0 }' /etc/group +awk -F: 'BEGIN { a=165536; b=231071 } + { x=$2; y=$2+$3-1; if (a<=y && x<=b) { print FILENAME ":" $0; found=1 } } + END { exit found ? 1 : 0 }' /etc/subuid /etc/subgid # 2. Install rootless prerequisites. docker-rootless-extras is an Arch AUR # package and must be built/reviewed under the administrator's normal AUR policy. @@ -97,45 +136,49 @@ sudo pacman -S --needed rootlesskit slirp4netns fuse-overlayfs shadow # Then install the reviewed docker-rootless-extras AUR package by the host's # approved AUR procedure; do not curl a remote install script into a shell. -# 3. Create the isolated identity. -sudo groupadd --gid 1200 le_payload_dev -sudo useradd --uid 1200 --gid le_payload_dev --create-home \ - --home-dir /home/le_payload_dev --shell /bin/bash le_payload_dev -sudo usermod --add-subuids 165536-231071 --add-subgids 165536-231071 le_payload_dev +# 3. Create the isolated primary group and project subvolume. +sudo groupadd --gid 1200 le_app_codex +sudo btrfs subvolume create /srv/le-app-codex +sudo chown root:le_app_codex /srv/le-app-codex +sudo chmod 0750 /srv/le-app-codex -# 4. Create a quota-capable project subvolume and ownership boundary. -sudo btrfs subvolume create /srv/le-payload-dev -sudo chown 1200:1200 /srv/le-payload-dev -sudo chmod 0700 /srv/le-payload-dev -sudo -u le_payload_dev mkdir -p \ - /srv/le-payload-dev/lasereverything.net.db \ - /srv/le-payload-dev/source \ - /srv/le-payload-dev/secrets/generated \ - /srv/le-payload-dev/runtime/docker \ - /srv/le-payload-dev/data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ - /srv/le-payload-dev/artifacts/{browser,migration-reports} +# 4. Create the account with its canonical home inside the project root. +sudo useradd --uid 1200 --gid le_app_codex --create-home \ + --home-dir /srv/le-app-codex/home --shell /bin/bash le_app_codex +sudo usermod --add-subuids 165536-231071 --add-subgids 165536-231071 le_app_codex + +# 5. Create the canonical children; no sibling workspace is created. +sudo mkdir -p \ + /srv/le-app-codex/lasereverything.net.db \ + /srv/le-app-codex/database-source-readonly \ + /srv/le-app-codex/nonproduction-secrets/generated \ + /srv/le-app-codex/container-runtime/docker \ + /srv/le-app-codex/testing-data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ + /srv/le-app-codex/build-artifacts/{browser,migration-reports} # Root owns the boundary and authoritative source; development owns mutable areas. -sudo chown root:le_payload_dev /srv/le-payload-dev -sudo chmod 0750 /srv/le-payload-dev -sudo chown -R le_payload_dev:le_payload_dev \ - /srv/le-payload-dev/lasereverything.net.db \ - /srv/le-payload-dev/runtime /srv/le-payload-dev/data /srv/le-payload-dev/artifacts \ - /srv/le-payload-dev/secrets/generated -sudo chmod 0700 /srv/le-payload-dev/runtime /srv/le-payload-dev/data \ - /srv/le-payload-dev/artifacts /srv/le-payload-dev/secrets/generated -sudo chown root:le_payload_dev /srv/le-payload-dev/source /srv/le-payload-dev/secrets -sudo chmod 0550 /srv/le-payload-dev/source -sudo chmod 0750 /srv/le-payload-dev/secrets +sudo chown root:le_app_codex /srv/le-app-codex +sudo chmod 0750 /srv/le-app-codex +sudo chown le_app_codex:le_app_codex /srv/le-app-codex/home +sudo chmod 0700 /srv/le-app-codex/home +sudo chown -R le_app_codex:le_app_codex \ + /srv/le-app-codex/lasereverything.net.db \ + /srv/le-app-codex/container-runtime /srv/le-app-codex/testing-data /srv/le-app-codex/build-artifacts \ + /srv/le-app-codex/nonproduction-secrets/generated +sudo chmod 0700 /srv/le-app-codex/container-runtime /srv/le-app-codex/testing-data \ + /srv/le-app-codex/build-artifacts /srv/le-app-codex/nonproduction-secrets/generated +sudo chown root:le_app_codex /srv/le-app-codex/database-source-readonly /srv/le-app-codex/nonproduction-secrets +sudo chmod 0550 /srv/le-app-codex/database-source-readonly +sudo chmod 0750 /srv/le-app-codex/nonproduction-secrets -# 5. Apply a 250 GiB ceiling to the project subvolume. +# 6. Apply a 250 GiB ceiling to the project subvolume. sudo btrfs quota enable /srv -sudo btrfs qgroup limit 250G /srv/le-payload-dev +sudo btrfs qgroup limit 250G /srv/le-app-codex -# 6. Transitional source access is used only by the administrator-run copy and +# 7. Transitional source access is used only by the administrator-run copy and # verification procedure below. The final account needs no ACL on /srv/codex-*. -# 7. Remediate production secret readability separately. +# 8. Remediate production secret readability separately. # Root-owned Compose can still read mode 0600 files. Confirm the production # operator before applying because this changes production file permissions. sudo chown root:root /var/www/lasereverything.net.db/app/.env.local @@ -143,21 +186,21 @@ sudo chmod 0600 /var/www/lasereverything.net.db/app/.env.local sudo chown root:root /var/www/lasereverything.net.db/bgbye/.env sudo chmod 0600 /var/www/lasereverything.net.db/bgbye/.env -# 8. As le_payload_dev, configure the rootless daemon with an explicit data root. -sudo -iu le_payload_dev mkdir -p ~/.config/docker +# 9. As le_app_codex, configure the rootless daemon with an explicit data root. +sudo -iu le_app_codex mkdir -p ~/.config/docker # Administrator writes ~/.config/docker/daemon.json as documented below, # then the dedicated user enables the packaged rootless user service. -sudo -iu le_payload_dev systemctl --user enable --now docker.socket +sudo -iu le_app_codex systemctl --user enable --now docker.socket -# 9. Optional only if reviewed stacks must survive logout. -sudo loginctl enable-linger le_payload_dev +# 10. Optional only if reviewed stacks must survive logout. +sudo loginctl enable-linger le_app_codex ``` -Proposed `/home/le_payload_dev/.config/docker/daemon.json`: +Proposed `/srv/le-app-codex/home/.config/docker/daemon.json`: ```json { - "data-root": "/srv/le-payload-dev/runtime/docker", + "data-root": "/srv/le-app-codex/container-runtime/docker", "live-restore": false, "log-driver": "local", "log-opts": { "max-size": "20m", "max-file": "5" } @@ -166,9 +209,40 @@ Proposed `/home/le_payload_dev/.config/docker/daemon.json`: The exact Arch rootless unit name (`docker.service` versus `docker.socket`) must be verified from the reviewed package before enabling. Cgroup v2 is present; after provisioning, verify the rootless/security options and delegated controllers before relying on CPU, memory, or PID limits. +The nonstandard home is compatible with rootless Docker and user-level systemd because it is the account home recorded in `/etc/passwd`, resides on local Btrfs, and is owned by the account. Required environment contract: + +- `HOME=/srv/le-app-codex/home` comes from the login/account record; +- `XDG_RUNTIME_DIR=/run/user/1200` is created by logind/systemd and must not be redirected into `/srv` or `/tmp`; +- `XDG_DATA_HOME=/srv/le-app-codex/home/.local/share`; +- Docker daemon data root is `/srv/le-app-codex/container-runtime/docker`; +- Docker client configuration is `/srv/le-app-codex/home/.docker`; +- user systemd configuration is `/srv/le-app-codex/home/.config/systemd/user`; +- the `le-app-codex` context points only to `unix:///run/user/1200/docker.sock`; +- wrappers export `COMPOSE_PROJECT_NAME=le-app-testing`. + +Proposed `/srv/le-app-codex/home/.config/environment.d/10-le-app-codex.conf`: + +```text +HOME=/srv/le-app-codex/home +XDG_DATA_HOME=/srv/le-app-codex/home/.local/share +DOCKER_CONFIG=/srv/le-app-codex/home/.docker +COMPOSE_PROJECT_NAME=le-app-testing +``` + +`XDG_RUNTIME_DIR` is deliberately absent from this file; logind must create `/run/user/1200` with the correct lifetime and ownership. `DOCKER_HOST` is also omitted so the named context remains the explicit authority and scripts can reject context drift. + +After the user service is installed, create the context as the dedicated account: + +```bash +sudo -iu le_app_codex docker context create le-app-codex \ + --description 'Laser Everything Codex nonproduction rootless runtime' \ + --docker host=unix:///run/user/1200/docker.sock +sudo -iu le_app_codex docker context use le-app-codex +``` + ## Required positive and negative verification -Run as `le_payload_dev`, printing only permissions/status: +Run as `le_app_codex`, printing only permissions/status: ```bash test ! -r /var/www/lasereverything.net.db/app/.env.local @@ -176,10 +250,10 @@ test ! -x /var/lib/docker test ! -x /var/lib/mysql test ! -r /var/run/docker.sock test ! -x /srv/directus-archive -test -r /srv/le-payload-dev/source/README.txt -test ! -w /srv/le-payload-dev/source/README.txt -test -r /srv/le-payload-dev/secrets/migration.env -test ! -w /srv/le-payload-dev/secrets/migration.env +test -r /srv/le-app-codex/database-source-readonly/README.txt +test ! -w /srv/le-app-codex/database-source-readonly/README.txt +test -r /srv/le-app-codex/nonproduction-secrets/migration.env +test ! -w /srv/le-app-codex/nonproduction-secrets/migration.env DOCKER_HOST=unix:///run/user/1200/docker.sock docker info --format '{{json .SecurityOptions}}' ``` @@ -192,12 +266,13 @@ Every stack/job wrapper will fail before Compose evaluation if: - effective UID is not the approved dedicated UID; - Docker endpoint is not exactly the approved rootless Unix socket; - Compose project name lacks the fixed nonproduction prefix; +- active Docker context is not exactly `le-app-codex`; - any resolved bind source is `/var/www`, `/var/lib/docker`, `/var/lib/mysql`, `/srv/directus-archive`, `/var/run`, `/run/docker.sock`, or a descendant; -- the Git checkout is not exactly `/srv/le-payload-dev/lasereverything.net.db` in nonproduction or `/var/www/lasereverything.net.db` in production; +- the Git checkout is not exactly `/srv/le-app-codex/lasereverything.net.db` in nonproduction or `/var/www/lasereverything.net.db` in production; - any non-finite application service receives `.migration.env`, `.migration-source`, a MariaDB source URL, or migration credentials; - destination database host/port is not the approved nonproduction target/service; - production-named secret scopes, SMTP hosts, OAuth variables, Ko-fi secrets, or webhook credentials appear in fixture/snapshot/rehearsal rendered Compose; -- reset/purge targets a bind mount, external volume, unknown project label, or any path outside `/srv/le-payload-dev`. +- reset/purge targets a bind mount, external volume, unknown project label, or any path outside `/srv/le-app-codex`. Tests render each Compose mode and inspect mounts, secrets, environment variable **names/classifications**, networks, contexts, labels, and volume ownership without printing values. @@ -206,12 +281,12 @@ Tests render each Compose mode and inspect mounts, secrets, environment variable - The authoritative MariaDB remains read-only at the database privilege and filesystem layers. - `.migration-source` and `.migration.env` are mounted read-only only into finite `inventory`, `import`, and `validation` jobs. - Frontend, Payload, BGBye, proxy, Mailpit, fixture seed, package installation, build, lint, unit, integration, and ordinary browser-test services never receive those mounts or variables. -- Legacy Directus restores the scrubbed SQL into a separate project-scoped writable MariaDB volume and copies uploads into `/srv/le-payload-dev/data/legacy-uploads` or a project volume. +- Legacy Directus restores the scrubbed SQL into a separate project-scoped writable MariaDB volume and copies uploads into `/srv/le-app-codex/testing-data/legacy-uploads` or a project volume. - Legacy Directus never mounts the source database storage or uploads, even read-only; the finite clone job is the only bridge. - Reset/purge operates only on labeled nonproduction destinations. It does not follow source symlinks and refuses all external/bind volumes. - `/srv/directus-archive` remains inaccessible and is never mentioned as a mount source in executable Compose configuration. -The complete root-only archive remains permanently outside the development boundary at `/srv/directus-archive/20260710-175408`. It is never moved, mounted, linked, or copied into `/srv/le-payload-dev`. +The complete root-only archive remains permanently outside the development boundary at `/srv/directus-archive/20260710-175408`. It is never moved, mounted, linked, or copied into `/srv/le-app-codex`. ## Nonproduction storage plan @@ -219,19 +294,19 @@ The host currently has approximately 650 GiB available on the Btrfs filesystem c | Path | Planning allowance | Ownership | Cleanup / backup | |---|---:|---|---| -| `runtime/docker` | 80 GiB soft budget | `le_payload_dev` | prune only project/unused development images after digest recording; no backup | -| `data/postgres` | 25 GiB | `le_payload_dev` / rootless mapped IDs | disposable; reset by labeled volume; no backup | -| `data/media` | 15 GiB | fixed container UID mapped by rootless daemon | fixture disposable; snapshot copied data disposable; no authoritative backup role | -| `data/legacy-uploads` | 10 GiB | rootless mapped service UID | copied from scrubbed source; disposable; never source of truth | -| `data/bgbye-models` | 35 GiB | BGBye service/rootless mapping | checksummed cache; LRU/manual cleanup; redownloadable | -| `data/tmp` | 10 GiB | service-specific mapped UID | tmpfs or age-based cleanup; no backup | -| `artifacts/browser` | 5 GiB | `le_payload_dev` | TTL 7 days; snapshot artifacts treated as sensitive and never committed | -| `artifacts/migration-reports` | 10 GiB | `le_payload_dev` | detailed reports TTL 30 days; committed summaries structural only | -| workspace/metadata headroom | 10 GiB | `le_payload_dev` | Git remote is source backup; no personal exports committed | +| `container-runtime/docker` | 80 GiB soft budget | `le_app_codex` | prune only project/unused development images after digest recording; no backup | +| `testing-data/postgres` | 25 GiB | `le_app_codex` / rootless mapped IDs | disposable; reset by labeled volume; no backup | +| `testing-data/media` | 15 GiB | fixed container UID mapped by rootless daemon | fixture disposable; snapshot copied data disposable; no authoritative backup role | +| `testing-data/legacy-uploads` | 10 GiB | rootless mapped service UID | copied from scrubbed source; disposable; never source of truth | +| `testing-data/bgbye-models` | 35 GiB | BGBye service/rootless mapping | checksummed cache; LRU/manual cleanup; redownloadable | +| `testing-data/tmp` | 10 GiB | service-specific mapped UID | tmpfs or age-based cleanup; no backup | +| `build-artifacts/browser` | 5 GiB | `le_app_codex` | TTL 7 days; snapshot artifacts treated as sensitive and never committed | +| `build-artifacts/migration-reports` | 10 GiB | `le_app_codex` | detailed reports TTL 30 days; committed summaries structural only | +| checkout/home metadata | 10 GiB | `le_app_codex` | Git remote is source backup; no personal exports committed | The listed soft budgets total 200 GiB. The 250 GiB hard qgroup ceiling reserves 50 GiB emergency headroom. Startup refuses when host free space is below 100 GiB, project usage exceeds 200 GiB, or required free space for a rehearsal cannot be reserved. The production filesystem is not used for spillover. -## Resources intentionally outside `/srv/le-payload-dev` +## Resources intentionally outside `/srv/le-app-codex` | Resource | Permanent location | Why it remains outside | |---|---|---| @@ -264,7 +339,7 @@ Metadata-only audit results at the planning checkpoint: | Resource | Current state | |---|---| | `/srv/codex-work/lasereverything.net.db` | Primary Git checkout, branch `temporary/utilities-only`, commit `305ea41`; owns the current local Git database | -| nested `.worktrees/payload-migration` | Migration worktree, branch `migration/payload`; planning commit begins at `ed1c84e` | +| nested `.worktrees/payload-migration` | Migration worktree, branch `migration/le-app-database`; planning commit begins at `ed1c84e` | | `.migration-source` | Symlink to `/srv/codex-migration/current` | | `.migration.env` | Symlink to `/srv/codex-secrets/le-payload-migration.env` | | `/srv/codex-migration/current` | Symlink to `source-20260710-175408` | @@ -297,7 +372,7 @@ No step deletes or mutates a transitional resource. Consolidation is copy/recrea ### Phase A — manifests and Git preservation -1. Push `migration/payload`, including planning commits, to `origin` and verify the remote commit. +1. Push `migration/le-app-database`, including planning commits, to `origin` and verify the remote commit. 2. Record structural manifests for every transitional path, symlink, owner/mode, aggregate size, Git branch/commit/remote, source checksum manifest, and the administrator container/volume inventory above. 3. Confirm the full root-only archive remains independently present and inaccessible; do not traverse or copy it. 4. Record the exact existing rootful container/volume names and leave them stopped/running exactly as found unless a later approved step says otherwise. @@ -307,18 +382,18 @@ No step deletes or mutates a transitional resource. Consolidation is copy/recrea After account/runtime approval, create one clean checkout as the dedicated user: ```bash -sudo -iu le_payload_dev git clone --branch migration/payload --single-branch \ +sudo -iu le_app_codex git clone --branch migration/le-app-database --single-branch \ ssh://FORGE/makearmy/le-app.git \ - /srv/le-payload-dev/lasereverything.net.db -sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ + /srv/le-app-codex/lasereverything.net.db +sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ fetch --prune origin -sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ +sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ status --short --branch -sudo -iu le_payload_dev git -C /srv/le-payload-dev/lasereverything.net.db \ +sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ rev-parse HEAD ``` -The HEAD must equal the reviewed remote `migration/payload` commit, the worktree must be clean, and no nested permanent worktree may exist. Provision a dedicated repository-scoped Git credential for `le_payload_dev`; do not copy `sol6_vi`'s private key. +The HEAD must equal the reviewed remote `migration/le-app-database` commit, the worktree must be clean, and no nested permanent worktree may exist. Provision a dedicated repository-scoped Git credential for `le_app_codex`; do not copy `sol6_vi`'s private key. ### Phase C — scrubbed source copy @@ -326,7 +401,7 @@ The administrator copies from the resolved source directory, never from the root ```bash source_old=/srv/codex-migration/source-20260710-175408 -source_new=/srv/le-payload-dev/source +source_new=/srv/le-app-codex/database-source-readonly sudo rsync -aHAX --numeric-ids --delete-delay "$source_old/" "$source_new/" @@ -335,18 +410,18 @@ sudo sh -c "cd '$source_new' && sha256sum --check --status CHECKSUMS.sha256" sudo sh -c "cd '$source_new' && sha256sum --check --status uploads.sha256" # Freeze the canonical input against development writes. -sudo chown -R root:le_payload_dev "$source_new" +sudo chown -R root:le_app_codex "$source_new" sudo find "$source_new" -type d -exec chmod 0550 {} + sudo find "$source_new" -type f -exec chmod 0440 {} + ``` -Before using `--delete-delay`, assert `source_new` resolves exactly beneath `/srv/le-payload-dev/source` and is not a symlink. The source checksum files, row-count manifests, schema, dump, extensions, and all uploads must validate. The old copy remains untouched. +Before using `--delete-delay`, assert `source_new` resolves exactly to `/srv/le-app-codex/database-source-readonly` and is not a symlink. The source checksum files, row-count manifests, schema, dump, extensions, and all uploads must validate. The old copy remains untouched. ### Phase D — nonproduction secrets - Preserve both old credential files in place during rollback. - Do not copy `le-payload-migration-admin.env`. -- Generate new fixture/snapshot/rootless database, Payload, Mailpit, and local gateway credentials directly into root-owned `/srv/le-payload-dev/secrets` with mode `0640`. +- Generate new fixture/snapshot/rootless database, Payload, Mailpit, and local gateway credentials directly into root-owned `/srv/le-app-codex/nonproduction-secrets` with mode `0640`. - If the MariaDB read-only credential from the scrubbed environment is temporarily required to compare the old rootful source, transfer only the minimum required variable set through an administrator-reviewed, non-logging process; replace it once the rootless source clone is verified. - No production SMTP, OAuth, Ko-fi, Directus token, webhook, or server credential enters the new directory. @@ -387,7 +462,7 @@ Every deletion requires a separate explicit approval showing the resolved path/o Promotion path: ```text -/srv/le-payload-dev/lasereverything.net.db +/srv/le-app-codex/lasereverything.net.db -> reviewed Git commit + signed/recorded release manifest -> CI-built immutable image digests tested in fixture/snapshot/rehearsal/staging -> clean checkout or verified release artifact at /var/www/lasereverything.net.db @@ -405,7 +480,7 @@ Required workflow: The production checkout contains all application source/configuration needed to reproduce the deployment contract. Physical image layers, declared production PostgreSQL/media volumes, and shared host-proxy configuration may remain outside it, but their names/locations/interfaces are declared in the production overlay and release manifest rooted there. -Forbidden promotion inputs include `/srv/le-payload-dev/runtime`, `source`, `secrets`, `data`, `artifacts`, local CA material, Mailpit state, test traces, rootless Docker state, disposable databases/volumes, caches, and development `.env` files. No `rsync` or recursive copy from `/srv/le-payload-dev` to `/var/www` is permitted. +Forbidden promotion inputs include `/srv/le-app-codex/container-runtime`, `database-source-readonly`, `nonproduction-secrets`, `testing-data`, `build-artifacts`, local CA material, Mailpit state, test traces, rootless Docker state, disposable databases/volumes, caches, and development `.env` files. No `rsync` or recursive copy from `/srv/le-app-codex` to `/var/www` is permitted. ## Local HTTPS and CA trust @@ -416,9 +491,9 @@ The local gateway uses a repository-pinned proxy image and an ignored, generated - `mail.le.localhost`; - optional `legacy.le.localhost`. -All bind to loopback port 8443, avoiding privileged ports and host DNS changes (`*.localhost` resolves locally). Certificates and private keys live under `/srv/le-payload-dev/secrets/local-ca`, mode `0700/0600`, excluded from Git and build contexts. +All bind to loopback port 8443, avoiding privileged ports and host DNS changes (`*.localhost` resolves locally). Certificates and private keys live under `/srv/le-app-codex/nonproduction-secrets/local-ca`, mode `0700/0600`, excluded from Git and build contexts. -Automated Playwright tests may use a dedicated browser profile with its own trust database. Manual Chromium trust can be limited to `/home/le_payload_dev/.pki/nssdb` using the already installed `certutil`; system-wide CA trust is unnecessary. Importing the CA into even that user/browser profile is a host/user-state change and requires explicit approval. Removal deletes the certificate from that profile and the ignored CA directory. +Automated Playwright tests may use a dedicated browser profile with its own trust database. Manual Chromium trust can be limited to `/srv/le-app-codex/home/.pki/nssdb` using the already installed `certutil`; system-wide CA trust is unnecessary. Importing the CA into even that user/browser profile is a host/user-state change and requires explicit approval. Removal deletes the certificate from that profile and the ignored CA directory. ## Mailpit and outbound protection @@ -465,28 +540,28 @@ CPU and GPU images share server contract tests. Capability differences appear in Rollback must be run only after confirming the selected context/socket is the dedicated one: ```bash -# As le_payload_dev: stop project resources, then the user daemon. +# As le_app_codex: stop project resources, then the user daemon. DOCKER_HOST=unix:///run/user/1200/docker.sock docker compose \ - --project-name le_payload_dev down --remove-orphans + --project-name le-app-testing down --remove-orphans systemctl --user disable --now docker.socket docker.service # As administrator: disable optional lingering. -sudo loginctl disable-linger le_payload_dev +sudo loginctl disable-linger le_app_codex # Verify production daemon endpoint and data were never referenced by project metadata/logs. # Use the planning preflight/audit script; do not connect to or mutate production Docker. # Remove only the dedicated project subvolume after an explicit artifact review. -sudo btrfs subvolume delete /srv/le-payload-dev +sudo btrfs subvolume delete /srv/le-app-codex # Remove subordinate allocations and account after the subvolume/runtime is gone. -sudo usermod --del-subuids 165536-231071 --del-subgids 165536-231071 le_payload_dev -sudo userdel --remove le_payload_dev -sudo groupdel le_payload_dev 2>/dev/null || true +sudo usermod --del-subuids 165536-231071 --del-subgids 165536-231071 le_app_codex +sudo userdel --remove le_app_codex +sudo groupdel le_app_codex 2>/dev/null || true ``` The dedicated Docker context lives only in the dedicated user's home and disappears with that home. If another operator created a client context pointing to the user socket, remove that context explicitly from that operator's client configuration. Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Any temporary transitional ACLs, if an administrator added them contrary to the final copy-based design, must be inventoried and removed before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, transitional source during its rollback retention, or `/srv/directus-archive`. -Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-payload-dev` resources were deleted. +Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-app-codex` resources were deleted. diff --git a/docs/payload-migration/implementation-plan.md b/docs/le-app-database-migration/implementation-plan.md similarity index 94% rename from docs/payload-migration/implementation-plan.md rename to docs/le-app-database-migration/implementation-plan.md index 2170132..67c90af 100644 --- a/docs/payload-migration/implementation-plan.md +++ b/docs/le-app-database-migration/implementation-plan.md @@ -1,4 +1,4 @@ -# Payload transition implementation plan +# Laser Everything database transition implementation plan Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runtime approval gate. @@ -9,7 +9,7 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt - Review `architecture-proposal.md`, `environment-parity.md`, and host prerequisites. 2. **Host runtime gate — requires human approval/provisioning** - - Provision the dedicated `le_payload_dev` account and a rootless Docker context owned only by it. + - Provision the dedicated `le_app_codex` account and a rootless Docker context owned only by it. - Restrict production environment files currently readable to `sol6_vi`; verify the development account cannot traverse production paths. - Use deterministic CPU BGBye initially; defer optional rootless GPU provisioning. - Approve browser-profile-scoped local CA trust for HTTPS testing. @@ -19,8 +19,8 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt 3. **Reversible canonical-path consolidation — separately approval-gated** - Inventory the transitional Git roots, symlinks, credential filenames, rootful disposable containers, and volumes without reading secret values. - - Create the sole checkout at `/srv/le-payload-dev/lasereverything.net.db` and verify `migration/payload` commit/remote state. - - Copy and checksum the scrubbed source into root-owned read-only `/srv/le-payload-dev/source`. + - Create the sole checkout at `/srv/le-app-codex/lasereverything.net.db` and verify `migration/le-app-database` commit/remote state. + - Copy and checksum the scrubbed source into root-owned read-only `/srv/le-app-codex/database-source-readonly`. - Create new nonproduction-only secrets and rootless MariaDB/PostgreSQL resources; do not reuse production credentials. - Preserve all old resources through the rollback period. Delete nothing without a later explicit approval. diff --git a/docs/payload-migration/metadata-disposition.md b/docs/le-app-database-migration/metadata-disposition.md similarity index 100% rename from docs/payload-migration/metadata-disposition.md rename to docs/le-app-database-migration/metadata-disposition.md From 13ec935e87cb25d9f477f2609bc95ed19390781b Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 15:02:29 -0400 Subject: [PATCH 06/14] correct contained Codex runtime plan --- .../environment-parity.md | 25 +- .../host-runtime-plan.md | 751 ++++++------------ .../implementation-plan.md | 11 +- 3 files changed, 275 insertions(+), 512 deletions(-) diff --git a/docs/le-app-database-migration/environment-parity.md b/docs/le-app-database-migration/environment-parity.md index 68ad642..485f016 100644 --- a/docs/le-app-database-migration/environment-parity.md +++ b/docs/le-app-database-migration/environment-parity.md @@ -140,29 +140,34 @@ Local HTTPS is required to test secure cookies. A repository-controlled developm ## Rootless Codex-controlled runtime -Required target: a dedicated rootless Docker daemon owned by the separate `le_app_codex` account. Future Codex migration sessions must execute as that account (preferred) or through a narrowly scoped account transition; the daemon socket is not shared broadly. Rootless Podman is an acceptable fallback only after Compose/profile behavior is proven; Docker is preferred because the repository and production already use Compose. +Required target: a genuine per-user rootless Docker service owned by `le_app_codex` and managed through the explicitly root-started `user@1200.service`. Root applies the filesystem, network-namespace, device, and resource boundary to the complete user manager so Codex, RootlessKit, Docker, and containers inherit it. The only accepted endpoint is `unix:///run/user/1200/docker.sock`; the daemon socket is not shared broadly. A system service running `dockerd` with `User=le_app_codex` is prohibited. Codex must be able to build images, create/remove only its project containers/networks/volumes, inspect their logs, reset disposable PostgreSQL, run finite jobs, and execute integration/browser tests. It must not access `/var/run/docker.sock`, the production daemon/context, production containers/volumes/networks, `/var/www`, or `/srv/directus-archive`. -Current host observation: +Current host observation and planning checkpoint: - `sol6_vi` is not in the Docker group but can read the production application's world-readable environment files under `/var/www`; - Docker and Docker Compose clients exist; - no Podman client is installed; - the default Docker context does not provide a usable daemon/security result to this user; -- no enabled user-level rootless Docker or Podman service was detected. +- no enabled user-level rootless Docker service was detected; +- no persistent `le-app-codex` namespace or project nftables policy is deployed; +- approved DNS remains unresolved; the host resolver currently includes `192.168.10.1`, which is an observation rather than approval; +- numerous existing Docker IPv4 and IPv6 bridge ranges must be dynamically inventoried and denied rather than represented only by a stale list. Therefore a `sol6_vi`-owned daemon is rejected: it could bind-mount those readable production secrets. Milestone 0 requires the isolated account and host changes described in `host-runtime-plan.md`, and work must stop before performing them. An administrator must approve/provision: 1. dedicated `le_app_codex` identity and restricted filesystem ACLs; -2. rootless prerequisites (`newuidmap`/`newgidmap`, subordinate UID/GID ranges, rootless networking and overlay helpers as required by the OS); -3. rootless Docker daemon installation for `le_app_codex` using its dedicated data root and named context; -4. optional user-service lingering if stacks must survive logout; -5. deterministic CPU BGBye first; optional GPU access only after core acceptance; -6. user/browser-profile-scoped development CA trust; -7. capacity limits for images, caches, snapshots, media, PostgreSQL, and test artifacts. +2. an explicitly selected package path: scheduled full host maintenance or a clean-chroot/current-system-compatible investigation, never a partial Arch upgrade; +3. a checksum-pinned Moby rootless launcher matched to the installed Docker release, not the unmodified AUR extras package or its global sysctl hook; +4. instance-specific containment on `user@1200.service`, the proposed `user-1200.slice` resource limits, and no lingering; +5. a root-created IPv4-only namespace with default-deny egress and dynamic denial of host, LAN, WireGuard, metadata, private, loopback, Docker bridge, SMTP, and IPv6 access; +6. inert acceptance tests before starting the user manager, Docker, or Codex; +7. deterministic CPU BGBye first; optional GPU access only after core acceptance; +8. user/browser-profile-scoped development CA trust; +9. a separately approved disk-growth limit for images, caches, snapshots, media, PostgreSQL, and test artifacts. -No production socket forwarding, Docker-group membership, broad sudo rule, daemon TCP exposure, or bind mount into production paths is acceptable. +Lingering must remain disabled. No production socket forwarding, Docker-group membership, broad sudo rule, daemon TCP exposure, system-service `dockerd`, or bind mount into production paths is acceptable. See `host-runtime-plan.md` for the approval-gated lifecycle and unresolved DNS, address-inventory, authentication, package, and disk-quota decisions. ## One-command workflows diff --git a/docs/le-app-database-migration/host-runtime-plan.md b/docs/le-app-database-migration/host-runtime-plan.md index 2482f48..f01bd29 100644 --- a/docs/le-app-database-migration/host-runtime-plan.md +++ b/docs/le-app-database-migration/host-runtime-plan.md @@ -1,567 +1,322 @@ # Milestone 0 host runtime and security plan -Status: planning-only. Commands in this document are proposed for administrator review. None have been executed. +Status: corrected planning checkpoint, 2026-07-12. Nothing in this document authorizes execution. Phase 1 created only the inert `le_app_codex` identity and directory skeleton. Package installation, systemd changes, namespace creation, firewall changes, user-manager startup, Docker startup, Codex authentication, repository/source copying, and production changes remain separately approval-gated. -## Permissions-only access audit +This document supersedes the earlier runtime proposal. In particular, the project will not use a system service running `dockerd` with `User=le_app_codex`, will not enable lingering, and will not install the unmodified AUR `docker-rootless-extras` package. -The audit inspected only existence, filesystem type, owner/mode, effective read/write/traverse permission, Docker context endpoints, and socket metadata. It did not read, print, copy, or inspect secret values, production databases, container metadata, or archive contents. +## Fixed production and trust boundaries -| Boundary | Effective access for `sol6_vi` | Finding | -|---|---|---| -| `/var/www` | read/traverse, not write | Production tree names and readable files are exposed to the account | -| `/var/www/lasereverything.net.db` and `app/` | read/traverse, not write | Production application and deployment configuration are readable | -| production `.env.local` and BGBye `.env` | read, not write; mode `0644` | **Unsafe for a `sol6_vi`-owned rootless daemon**; values were not read | -| `/var/lib/docker` | no read/write/traverse | Production Docker data is denied by host permissions | -| `/var/lib/mysql` | no read/write/traverse | Production database storage is denied | -| `/var/run/docker.sock` and `/run/docker.sock` | no read/write | Production daemon is denied; only the `default` context points to it | -| `/srv/directus-archive` | no read/write/traverse | Root-only archive boundary works; timestamped child is inaccessible | -| `/srv/codex-migration` | read/traverse, not write | Scrubbed migration source is available read-only | -| `/srv/codex-secrets` | read/traverse at directory level, not write | Migration env is readable as intended; access must be narrowed for the new account to the one nonproduction file | -| workspace | read/write | Current development tree is owned by `sol6_vi` | +- Production remains rooted at `/var/www/lasereverything.net.db`. +- The complete archive remains root-only at `/srv/directus-archive/20260710-175408` and is never moved, mounted, linked, copied, or exposed to Codex. +- The contained nonproduction root is `/srv/le-app-codex`. +- Transitional `/srv/codex-work`, `/srv/codex-migration`, and `/srv/codex-secrets` resources remain outside the contained environment until an independently approved copy-and-verify stage. +- The rootful Docker and containerd sockets and data directories are inaccessible to the contained environment. +- The only accepted project daemon endpoint is `unix:///run/user/1200/docker.sock`. +- No Docker TCP/SSH endpoint, production context, shared Docker group, socket forwarding, or fallback to `/run/docker.sock` or `/var/run/docker.sock` is accepted. -Conclusion: rootless Docker owned by `sol6_vi` would protect the production daemon but not production files already readable to that user. Any controller of that daemon could mount the readable production environment files. The production secret file modes also deserve independent remediation. +## Confirmed Phase 1 state -## Runtime account model comparison +Phase 1 created: -| Model | Advantages | Risks / cost | Decision | -|---|---|---|---| -| `sol6_vi` owns rootless Docker | Minimal provisioning; current workspace already writable | Can bind-mount world-readable production secrets and application files; mixes general Codex work with container authority | Rejected on this host | -| dedicated `le_app_codex` | Daemon owner has an explicit filesystem allow-list; project storage and cleanup are attributable; production paths can be proven inaccessible | Requires account, ACL, subordinate IDs, separate Git credentials/session, storage, and user service | **Recommended** | +- user/group `le_app_codex`, UID/GID `1200`, primary group only; +- locked password, `/usr/bin/nologin`, and no supplementary groups; +- subordinate UID/GID allocation `165536:65536`; +- inert workspace skeleton under `/srv/le-app-codex` with root-owned boundaries and dedicated-user mutable children. -Future autonomous migration work should run in a Codex session whose OS identity is `le_app_codex`. Do not grant `sol6_vi` generic access to the development daemon socket as a convenience; that weakens accountability. If orchestration must be initiated from another account, use a narrowly audited command wrapper or SSH into `le_app_codex`, not a shared Docker group and not TCP exposure of the daemon. +Phase 1 did not install packages, start `user@1200.service`, start a daemon or container, create a network namespace, change nftables, authenticate Codex, copy a repository or migration source, or touch production. Lingering was not enabled and remains prohibited. Do not rerun any Phase 1 artifact. -## Exact proposed identity and filesystem model +## Correct runtime lifecycle -Proposed fixed identity: +Docker is a genuine per-user service defined below `/srv/le-app-codex/home/.config/systemd/user`. Its lifetime is managed by the systemd user manager for UID 1200. -- user and primary group: `le_app_codex`; -- UID/GID: `1200` (verified unused at audit time; recheck immediately before creation); -- shell/home: `/bin/bash`, `/srv/le-app-codex/home`; -- no supplementary production groups, no `docker` group, no sudo/wheel membership; -- subordinate UID and GID range: `165536:65536`, non-overlapping with the observed `sol6_ii:100000:65536` allocation; -- dedicated project root: Btrfs subvolume `/srv/le-app-codex`, owner `root:le_app_codex`, mode `0750`; -- sole checkout: `/srv/le-app-codex/lasereverything.net.db`; -- authoritative scrubbed input: `/srv/le-app-codex/database-source-readonly`, root-owned and read-only to the development group; -- nonproduction credentials only: `/srv/le-app-codex/nonproduction-secrets`; -- daemon data root: `/srv/le-app-codex/container-runtime/docker`; -- runtime socket: `/run/user/1200/docker.sock`; -- Docker client/config: `/srv/le-app-codex/home/.docker` and `/srv/le-app-codex/home/.config/docker`; -- rootless Docker context: `le-app-codex`; -- default nonproduction Compose project: `le-app-testing`, enforced by wrapper preflight. - -Verified candidate availability at this planning checkpoint: - -- neither account nor group name `le_app_codex` exists; -- UID `1200` is unused in `/etc/passwd` and GID `1200` is unused in `/etc/group`; -- no passwd primary UID/GID or group GID collision exists at `1200`; -- `/etc/subuid` and `/etc/subgid` contain one observed allocation, `100000–165535`, owned by the historical `sol6_ii` entry; -- candidate range `165536–231071` does not overlap any entry in either subordinate-ID file; -- `/srv/le-app-codex` does not exist. - -Therefore the current exact recommendation is UID/GID `1200` and subordinate range `165536:65536`, subject to rerunning the documented collision checks immediately before provisioning. If any check produces output or a non-free result, stop and select new IDs; do not force reuse. - -## Canonical naming replacements - -| Superseded proposal | Canonical replacement | -|---|---| -| account/group `le_payload_dev` | `le_app_codex` | -| nonproduction root `/srv/le-payload-dev` | `/srv/le-app-codex` | -| checkout `/srv/le-payload-dev/lasereverything.net.db` | `/srv/le-app-codex/lasereverything.net.db` | -| source directory `source` | `database-source-readonly` | -| secrets directory `secrets` | `nonproduction-secrets` | -| runtime directory `runtime` | `container-runtime` | -| disposable-data directory `data` | `testing-data` | -| artifact directory `artifacts` | `build-artifacts` | -| Docker context `le-payload-dev` | `le-app-codex` | -| Compose project `le_payload_dev` | `le-app-testing` | -| branch `migration/payload` | `migration/le-app-database` | -| documentation `docs/payload-migration` | `docs/le-app-database-migration` | - -`payload` remains valid only for the actual Payload CMS service, application directory, packages, configuration, and Payload-specific migrations. Historical inventory names such as `le-payload-pg` and `.worktrees/payload-migration` remain unchanged because they identify real transitional resources. - -Canonical nonproduction tree: +Root enforces containment around the complete user manager with instance-specific configuration for: ```text -/srv/le-app-codex/ -├── home/ # dedicated account home and user configuration -├── lasereverything.net.db/ # sole persistent nonproduction Git checkout -├── database-source-readonly/ # root-owned scrubbed authoritative input -├── nonproduction-secrets/ # testing/development credentials only -├── container-runtime/ # rootless Docker images, layers and configuration -├── testing-data/ # disposable databases, media, models and caches -└── build-artifacts/ # reports, logs and browser/test evidence +/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf +/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf ``` -Ownership and permissions: +The user manager depends on the root-created network namespace and enters that namespace before any user unit starts. Consequently Codex, RootlessKit, `dockerd`, containerd, build processes, test jobs, and containers inherit the same root-enforced filesystem, network, device, and cgroup boundary. -| Path | Owner:group | Mode/policy | -|---|---|---| -| `/srv/le-app-codex` | `root:le_app_codex` | `0750`; prevents unrelated users and prevents project-root restructuring by the development account | -| `home` | `le_app_codex:le_app_codex` | `0700`; account, Docker client, user systemd, NSS/browser configuration | -| `lasereverything.net.db` | `le_app_codex:le_app_codex` | directories `0750`, normal Git files subject to repository modes; sole checkout | -| `database-source-readonly` | `root:le_app_codex` | directories `0550`, files `0440`; account and daemon may read but cannot alter; no writable symlink target | -| `nonproduction-secrets` | `root:le_app_codex` | directory `0750`, provisioned files `0640`; nonproduction values only; optional `generated/` is `0700 le_app_codex` for ephemeral fixture secrets | -| `container-runtime` | `le_app_codex:le_app_codex` | `0700`; rootless Docker images, layers and configuration only | -| `testing-data` | `le_app_codex:le_app_codex` | `0700`; child ownership may use rootless subordinate IDs | -| `build-artifacts` | `le_app_codex:le_app_codex` | `0700`; TTL cleanup; snapshot artifacts treated as sensitive | +Root starts and stops `user@1200.service` explicitly after inert acceptance tests pass. Lingering is prohibited and must remain disabled. The Docker user unit may be enabled for the user manager's controlled lifetime, but must not cause the manager to start independently. -Only the dedicated account receives: +A system unit with `User=le_app_codex` must not run `dockerd`. That model bypasses the supported per-user lifecycle and does not provide the required containment of Codex and every sibling user process. -- read/write access to its workspace and runtime/storage subtree; -- group read/traverse access to canonical root-owned `database-source-readonly`; -- group read access to specifically provisioned files under canonical `nonproduction-secrets`; -- a separate nonproduction Git credential/deploy key with access limited to the application repository. +## Package decision gate and pinned launcher -It receives no ACL or group membership for `/var/www`, production database paths, production sockets/volumes, or `/srv/directus-archive`. +The last isolated preview of: -## Exact privileged prerequisites and commands - -These commands are a reviewable proposal for an administrator on Garuda/Arch Linux. Recheck UID/GID and subordinate ranges first. Do not paste secrets into the shell history. - -```bash -# 1. Recheck names, IDs, and ranges across all four account databases. -getent passwd le_app_codex -getent group le_app_codex -getent passwd 1200 -getent group 1200 -awk -F: '$3 == 1200 || $4 == 1200 { print; found=1 } END { exit found ? 1 : 0 }' /etc/passwd -awk -F: '$3 == 1200 { print; found=1 } END { exit found ? 1 : 0 }' /etc/group -awk -F: 'BEGIN { a=165536; b=231071 } - { x=$2; y=$2+$3-1; if (a<=y && x<=b) { print FILENAME ":" $0; found=1 } } - END { exit found ? 1 : 0 }' /etc/subuid /etc/subgid - -# 2. Install rootless prerequisites. docker-rootless-extras is an Arch AUR -# package and must be built/reviewed under the administrator's normal AUR policy. -sudo pacman -S --needed rootlesskit slirp4netns fuse-overlayfs shadow -# Then install the reviewed docker-rootless-extras AUR package by the host's -# approved AUR procedure; do not curl a remote install script into a shell. - -# 3. Create the isolated primary group and project subvolume. -sudo groupadd --gid 1200 le_app_codex -sudo btrfs subvolume create /srv/le-app-codex -sudo chown root:le_app_codex /srv/le-app-codex -sudo chmod 0750 /srv/le-app-codex - -# 4. Create the account with its canonical home inside the project root. -sudo useradd --uid 1200 --gid le_app_codex --create-home \ - --home-dir /srv/le-app-codex/home --shell /bin/bash le_app_codex -sudo usermod --add-subuids 165536-231071 --add-subgids 165536-231071 le_app_codex - -# 5. Create the canonical children; no sibling workspace is created. -sudo mkdir -p \ - /srv/le-app-codex/lasereverything.net.db \ - /srv/le-app-codex/database-source-readonly \ - /srv/le-app-codex/nonproduction-secrets/generated \ - /srv/le-app-codex/container-runtime/docker \ - /srv/le-app-codex/testing-data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ - /srv/le-app-codex/build-artifacts/{browser,migration-reports} - -# Root owns the boundary and authoritative source; development owns mutable areas. -sudo chown root:le_app_codex /srv/le-app-codex -sudo chmod 0750 /srv/le-app-codex -sudo chown le_app_codex:le_app_codex /srv/le-app-codex/home -sudo chmod 0700 /srv/le-app-codex/home -sudo chown -R le_app_codex:le_app_codex \ - /srv/le-app-codex/lasereverything.net.db \ - /srv/le-app-codex/container-runtime /srv/le-app-codex/testing-data /srv/le-app-codex/build-artifacts \ - /srv/le-app-codex/nonproduction-secrets/generated -sudo chmod 0700 /srv/le-app-codex/container-runtime /srv/le-app-codex/testing-data \ - /srv/le-app-codex/build-artifacts /srv/le-app-codex/nonproduction-secrets/generated -sudo chown root:le_app_codex /srv/le-app-codex/database-source-readonly /srv/le-app-codex/nonproduction-secrets -sudo chmod 0550 /srv/le-app-codex/database-source-readonly -sudo chmod 0750 /srv/le-app-codex/nonproduction-secrets - -# 6. Apply a 250 GiB ceiling to the project subvolume. -sudo btrfs quota enable /srv -sudo btrfs qgroup limit 250G /srv/le-app-codex - -# 7. Transitional source access is used only by the administrator-run copy and -# verification procedure below. The final account needs no ACL on /srv/codex-*. - -# 8. Remediate production secret readability separately. -# Root-owned Compose can still read mode 0600 files. Confirm the production -# operator before applying because this changes production file permissions. -sudo chown root:root /var/www/lasereverything.net.db/app/.env.local -sudo chmod 0600 /var/www/lasereverything.net.db/app/.env.local -sudo chown root:root /var/www/lasereverything.net.db/bgbye/.env -sudo chmod 0600 /var/www/lasereverything.net.db/bgbye/.env - -# 9. As le_app_codex, configure the rootless daemon with an explicit data root. -sudo -iu le_app_codex mkdir -p ~/.config/docker -# Administrator writes ~/.config/docker/daemon.json as documented below, -# then the dedicated user enables the packaged rootless user service. -sudo -iu le_app_codex systemctl --user enable --now docker.socket - -# 10. Optional only if reviewed stacks must survive logout. -sudo loginctl enable-linger le_app_codex +```text +pacman -Syu --needed rootlesskit slirp4netns openai-codex ``` -Proposed `/srv/le-app-codex/home/.config/docker/daemon.json`: +was a 221-package full host upgrade affecting both installed kernels and OpenSSH. It is a host-maintenance operation requiring a fresh exact transaction preview, reboot planning, service-health checks, `.pacnew` review, and separate operator approval. A partial Arch upgrade is not acceptable. + +The unresolved package paths are: + +1. schedule and approve the complete refreshed host upgrade; or +2. investigate clean-chroot, checksum-pinned packages compatible with the current system, without partially upgrading repository packages. + +Do not install the unmodified AUR `docker-rootless-extras` package. Its install hook reloads global sysctls, which exceeds this project's boundary. Do not curl an installer into a shell. + +Install only a reviewed Moby `contrib/dockerd-rootless.sh` matching the Docker engine actually installed after the chosen package path. At the current checkpoint Docker is `29.6.1`; the candidate is: + +```text +Moby tag: docker-v29.6.1 +Source: contrib/dockerd-rootless.sh +Target: /usr/local/libexec/dockerd-rootless-29.6.1.sh +SHA-256: 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46 +Owner/mode: root:root 0755 +``` + +Revalidate the installed Docker version, upstream tag, source content, checksum, and launcher compatibility immediately before installation. If Docker changes, this filename and checksum are invalid until a matching launcher is reviewed. A clean-chroot package containing only the pinned launcher is acceptable; it must have no global sysctl hook, setup tool, service enablement, or post-install execution. + +## User-manager filesystem and device containment + +The proposed, not deployed, containment contract is: + +```ini +# /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf +[Unit] +Requires=le-app-codex-netns.service +After=le-app-codex-netns.service + +[Service] +NetworkNamespacePath=/run/netns/le-app-codex +ProtectSystem=strict +PrivateTmp=yes + +TemporaryFileSystem=/srv:ro +BindPaths=/srv/le-app-codex:/srv/le-app-codex +TemporaryFileSystem=/var:ro +TemporaryFileSystem=/mnt:ro +TemporaryFileSystem=/media:ro +TemporaryFileSystem=/opt:ro + +InaccessiblePaths=/var/www +InaccessiblePaths=/root +InaccessiblePaths=/home +InaccessiblePaths=/boot +InaccessiblePaths=/efi +InaccessiblePaths=/run/docker.sock +InaccessiblePaths=/var/run/docker.sock +InaccessiblePaths=/run/containerd/containerd.sock +InaccessiblePaths=/run/podman +InaccessiblePaths=/run/dbus/system_bus_socket +InaccessiblePaths=/run/systemd/private +InaccessiblePaths=/var/lib/docker +InaccessiblePaths=/var/lib/containerd +InaccessiblePaths=/var/lib/containers +InaccessiblePaths=/etc/docker +InaccessiblePaths=/etc/containers +InaccessiblePaths=/etc/ssh +InaccessiblePaths=/etc/NetworkManager/system-connections +InaccessiblePaths=/etc/wireguard + +DevicePolicy=closed +DeviceAllow=/dev/fuse rw +DeviceAllow=/dev/net/tun rw +``` + +Expected visibility is read-only runtime content under `/usr`, `/bin`, and `/lib*`; selected nonsensitive read-only `/etc`; normal `/proc` pending acceptance tests; read-only `/sys`; a closed device policy with FUSE/TUN; `/run/user/1200`; a private `/srv` exposing only `/srv/le-app-codex`; empty private `/var`, `/mnt`, `/media`, and `/opt`; and private `/tmp`. + +The following are intentionally deferred because they may break subordinate-ID mapping, user-manager behavior, cgroup delegation, or container creation: + +```ini +NoNewPrivileges=yes +RestrictSUIDSGID=yes +ProtectKernelTunables=yes +ProtectControlGroups=yes +ProcSubset=pid +``` + +`ProtectProc=invisible` and hostname isolation remain acceptance-test candidates. They may be added only after the user manager, RootlessKit, Docker, Codex, and test containers pass with them. The final contract must prevent unrelated host-process inspection without breaking the supported runtime. + +## Resource boundary + +The proposed, not deployed, aggregate boundary for the user manager and everything below it is: + +```ini +# /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf +[Slice] +CPUQuota=600% +MemoryHigh=48G +MemoryMax=64G +MemorySwapMax=16G +TasksMax=4096 +``` + +These limits cover Codex, RootlessKit, `dockerd`, containerd, builds, jobs, and all containers. Validate generated properties and cgroup delegation before relying on the limits. + +Disk-growth containment remains unresolved. Btrfs quotas were disabled at the last observation. Do not enable quotas, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit as an incidental provisioning step. Filesystem visibility constrains where the account may write but does not cap growth within `/srv/le-app-codex`. The operator must separately choose and approve a disk-quota policy. + +Soft planning budgets remain useful for monitoring: 80 GiB container runtime, 25 GiB PostgreSQL, 15 GiB media, 10 GiB legacy uploads, 35 GiB BGBye models, 10 GiB temporary data, 5 GiB browser artifacts, 10 GiB migration reports, and 10 GiB checkout/home metadata. Startup should fail when host/project free-space thresholds are unsafe, but soft checks do not replace a hard quota. + +## Rootless Docker user service + +The proposed, not deployed, user unit is: + +```ini +# /srv/le-app-codex/home/.config/systemd/user/docker.service +[Unit] +Description=Laser Everything rootless Docker +Documentation=https://docs.docker.com/engine/security/rootless/ + +[Service] +Type=notify +NotifyAccess=all +KillMode=mixed +Environment=HOME=/srv/le-app-codex/home +Environment=XDG_RUNTIME_DIR=/run/user/1200 +Environment=DOCKER_HOST=unix:///run/user/1200/docker.sock +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=/run/user/1200/dockerd-rootless +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=builtin +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520 +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=false +ExecStart=/usr/local/libexec/dockerd-rootless-29.6.1.sh +ExecReload=/usr/bin/kill -s HUP $MAINPID +Restart=always +RestartSec=2 +TimeoutStartSec=0 +TimeoutStopSec=120 +LimitNOFILE=infinity +LimitNPROC=infinity +LimitCORE=infinity +TasksMax=infinity +Delegate=yes + +[Install] +WantedBy=default.target +``` + +The `ExecStart` path must be updated if the installed Docker release changes. Proposed daemon configuration at `/srv/le-app-codex/home/.config/docker/daemon.json`: ```json { "data-root": "/srv/le-app-codex/container-runtime/docker", + "storage-driver": "fuse-overlayfs", "live-restore": false, + "userland-proxy": false, + "ipv6": false, + "ip6tables": false, "log-driver": "local", - "log-opts": { "max-size": "20m", "max-file": "5" } + "log-opts": { + "max-size": "20m", + "max-file": "5" + } } ``` -The exact Arch rootless unit name (`docker.service` versus `docker.socket`) must be verified from the reviewed package before enabling. Cgroup v2 is present; after provisioning, verify the rootless/security options and delegated controllers before relying on CPU, memory, or PID limits. +Docker client configuration lives only below `/srv/le-app-codex/home/.docker`. The named `le-app-codex` context must resolve exactly to `unix:///run/user/1200/docker.sock`. Wrappers reject the `default` context, rootful sockets, TCP/SSH endpoints, and unknown contexts. -The nonstandard home is compatible with rootless Docker and user-level systemd because it is the account home recorded in `/etc/passwd`, resides on local Btrfs, and is owned by the account. Required environment contract: +## IPv4-only network containment -- `HOME=/srv/le-app-codex/home` comes from the login/account record; -- `XDG_RUNTIME_DIR=/run/user/1200` is created by logind/systemd and must not be redirected into `/srv` or `/tmp`; -- `XDG_DATA_HOME=/srv/le-app-codex/home/.local/share`; -- Docker daemon data root is `/srv/le-app-codex/container-runtime/docker`; -- Docker client configuration is `/srv/le-app-codex/home/.docker`; -- user systemd configuration is `/srv/le-app-codex/home/.config/systemd/user`; -- the `le-app-codex` context points only to `unix:///run/user/1200/docker.sock`; -- wrappers export `COMPOSE_PROJECT_NAME=le-app-testing`. +Root creates and owns the persistent namespace `/run/netns/le-app-codex`. The complete `user@1200.service` enters it through `NetworkNamespacePath`; the user cannot replace the namespace or firewall policy. -Proposed `/srv/le-app-codex/home/.config/environment.d/10-le-app-codex.conf`: +Current read-only findings at this checkpoint: -```text -HOME=/srv/le-app-codex/home -XDG_DATA_HOME=/srv/le-app-codex/home/.local/share -DOCKER_CONFIG=/srv/le-app-codex/home/.docker -COMPOSE_PROJECT_NAME=le-app-testing -``` +- approved DNS resolver IPv4 addresses remain unresolved; +- the host resolver currently includes `192.168.10.1`, but observation is not approval; +- no persistent `le-app-codex` network namespace currently exists; +- no project-specific nftables rules are deployed; +- the host has numerous existing Docker IPv4 and IPv6 bridge ranges. -`XDG_RUNTIME_DIR` is deliberately absent from this file; logind must create `/run/user/1200` with the correct lifetime and ownership. `DOCKER_HOST` is also omitted so the named context remains the explicit authority and scripts can reject context drift. +All host addresses, routes, LAN/VPN/WireGuard/management networks, public/NAT addresses, Docker bridges, and resolver endpoints must be dynamically inventoried immediately before drafting the final rules. Future policy must deny every observed Docker IPv4 and IPv6 bridge range and must not rely only on a stale hard-coded list. -After the user service is installed, create the context as the dedicated account: +The namespace is IPv4-only: assign no IPv6 address or route and reject IPv6 on the project veth. Do not change host-global IPv6 or sysctls. -```bash -sudo -iu le_app_codex docker context create le-app-codex \ - --description 'Laser Everything Codex nonproduction rootless runtime' \ - --docker host=unix:///run/user/1200/docker.sock -sudo -iu le_app_codex docker context use le-app-codex -``` +The nftables design is default-deny and must cover namespace egress and host-to-namespace ingress in `FORWARD`, veth-gateway traffic in host `INPUT`, established/related replies, UDP and TCP DNS, and RootlessKit port-publication attempts. Initially no inbound forwarding to the namespace is allowed; a container requesting `-p` must remain unreachable from the host, LAN, VPN, and public networks. -## Required positive and negative verification +Permit only: -Run as `le_app_codex`, printing only permissions/status: +- established/related reply traffic; +- UDP/TCP DNS to explicitly approved resolver IPv4 addresses; +- TCP 80/443 to public destinations after exclusions. -```bash -test ! -r /var/www/lasereverything.net.db/app/.env.local -test ! -x /var/lib/docker -test ! -x /var/lib/mysql -test ! -r /var/run/docker.sock -test ! -x /srv/directus-archive -test -r /srv/le-app-codex/database-source-readonly/README.txt -test ! -w /srv/le-app-codex/database-source-readonly/README.txt -test -r /srv/le-app-codex/nonproduction-secrets/migration.env -test ! -w /srv/le-app-codex/nonproduction-secrets/migration.env -DOCKER_HOST=unix:///run/user/1200/docker.sock docker info --format '{{json .SecurityOptions}}' -``` +Explicitly reject: -The final line must show rootless security. The development context must resolve only to `/run/user/1200/docker.sock`; wrappers reject `default`, `/var/run/docker.sock`, TCP endpoints, SSH contexts, and any unknown endpoint. +- namespace and host loopback ranges; +- host veth gateway except a separately approved DNS/proxy endpoint; +- all host addresses, LAN, management, VPN, and WireGuard addresses/subnets; +- every current and later-inventoried Docker/Podman/container bridge range; +- RFC1918 private ranges, carrier-grade NAT, link-local, metadata including `169.254.169.254`, multicast, benchmark, documentation, reserved, and other non-public ranges; +- every TITANSERVER public/NAT address supplied by the operator; +- TCP 25, 465, and 587; +- all IPv6 traffic. -## Production path and socket denial preflight +Known addresses requiring explicit coverage include `192.168.10.151`, `10.98.0.2`, and `2603:7080:7500:1279:75aa:d64d:4cf0:8f10`; these are not a complete inventory. -Every stack/job wrapper will fail before Compose evaluation if: +Required operator inputs are approved DNS resolver IPv4 addresses, the complete host/public/NAT address set, every LAN/VPN/WireGuard/bridge/management subnet, and any approved host DNS/proxy endpoint. Do not finalize or deploy nftables without those inputs and a fresh read-only ruleset inventory. Forge SSH is outside the sandbox; no TCP/22 or LAN exception is proposed. -- effective UID is not the approved dedicated UID; -- Docker endpoint is not exactly the approved rootless Unix socket; -- Compose project name lacks the fixed nonproduction prefix; -- active Docker context is not exactly `le-app-codex`; -- any resolved bind source is `/var/www`, `/var/lib/docker`, `/var/lib/mysql`, `/srv/directus-archive`, `/var/run`, `/run/docker.sock`, or a descendant; -- the Git checkout is not exactly `/srv/le-app-codex/lasereverything.net.db` in nonproduction or `/var/www/lasereverything.net.db` in production; -- any non-finite application service receives `.migration.env`, `.migration-source`, a MariaDB source URL, or migration credentials; -- destination database host/port is not the approved nonproduction target/service; -- production-named secret scopes, SMTP hosts, OAuth variables, Ko-fi secrets, or webhook credentials appear in fixture/snapshot/rehearsal rendered Compose; -- reset/purge targets a bind mount, external volume, unknown project label, or any path outside `/srv/le-app-codex`. +## Acceptance gates and order of operations -Tests render each Compose mode and inspect mounts, secrets, environment variable **names/classifications**, networks, contexts, labels, and volume ownership without printing values. +Every stage stops for review. No real migration data, nonproduction credentials, or Codex authentication enters the boundary until inert tests pass. -## Authoritative migration-source protection +1. Resolve and approve the package path; install only the approved packages in a separately controlled stage. +2. If a full host upgrade was selected, reboot and complete production health verification. +3. Install only the version-matched, checksum-pinned Moby launcher. +4. Install the resource slice and user-manager containment files without starting `user@1200.service`. +5. Create the namespace without Docker; inventory generated properties, mounts, routes, addresses, and namespace identity. +6. Install the reviewed nftables fragment without Docker and test positive DNS/HTTPS plus every negative route class. +7. Run inert one-shot processes through the proposed user-manager namespace to validate filesystem, `/proc`, devices, cgroups, symlink/bind-mount resistance, and network denial. +8. Only after the inert gates pass, root explicitly starts `user@1200.service`. +9. Install/start the Docker user service and validate the exact socket, rootless security, data root, storage driver, logs, resource accounting, and port-publication denial. +10. Run a disposable container through the same positive and negative tests, then tear it down. +11. Only after runtime acceptance, choose and perform Codex authentication. +12. Repository cloning, scrubbed-source copying, new secret generation, and Milestone 0 implementation are later separately reviewed stages. -- The authoritative MariaDB remains read-only at the database privilege and filesystem layers. -- `.migration-source` and `.migration.env` are mounted read-only only into finite `inventory`, `import`, and `validation` jobs. -- Frontend, Payload, BGBye, proxy, Mailpit, fixture seed, package installation, build, lint, unit, integration, and ordinary browser-test services never receive those mounts or variables. -- Legacy Directus restores the scrubbed SQL into a separate project-scoped writable MariaDB volume and copies uploads into `/srv/le-app-codex/testing-data/legacy-uploads` or a project volume. -- Legacy Directus never mounts the source database storage or uploads, even read-only; the finite clone job is the only bridge. -- Reset/purge operates only on labeled nonproduction destinations. It does not follow source symlinks and refuses all external/bind volumes. -- `/srv/directus-archive` remains inaccessible and is never mentioned as a mount source in executable Compose configuration. +Positive tests must prove that the user manager, Codex process, daemon, and disposable container can write only approved paths below `/srv/le-app-codex`, use only `/run/user/1200/docker.sock`, store Docker data only below `container-runtime/docker`, resolve through approved DNS, and reach approved public HTTPS endpoints. -The complete root-only archive remains permanently outside the development boundary at `/srv/directus-archive/20260710-175408`. It is never moved, mounted, linked, or copied into `/srv/le-app-codex`. +Negative tests must prove they cannot list/read `/var/www`, `/root`, real `/home`, `/srv/directus-archive`, or transitional `/srv/codex-*`; open rootful runtime sockets or data; reach host/LAN/WireGuard/private/link-local/metadata/bridge/reserved destinations; use direct SMTP or IPv6; publish a port to the host/LAN; bypass controls via symlinks or bind mounts; inspect unrelated host processes; or escape resource limits. -## Nonproduction storage plan +Failure of any negative test is a hard stop, not a reason to weaken containment ad hoc. -The host currently has approximately 650 GiB available on the Btrfs filesystem containing `/srv` and `/home`. Proposed aggregate project quota: 250 GiB. +## Codex authentication decision -| Path | Planning allowance | Ownership | Cleanup / backup | -|---|---:|---|---| -| `container-runtime/docker` | 80 GiB soft budget | `le_app_codex` | prune only project/unused development images after digest recording; no backup | -| `testing-data/postgres` | 25 GiB | `le_app_codex` / rootless mapped IDs | disposable; reset by labeled volume; no backup | -| `testing-data/media` | 15 GiB | fixed container UID mapped by rootless daemon | fixture disposable; snapshot copied data disposable; no authoritative backup role | -| `testing-data/legacy-uploads` | 10 GiB | rootless mapped service UID | copied from scrubbed source; disposable; never source of truth | -| `testing-data/bgbye-models` | 35 GiB | BGBye service/rootless mapping | checksummed cache; LRU/manual cleanup; redownloadable | -| `testing-data/tmp` | 10 GiB | service-specific mapped UID | tmpfs or age-based cleanup; no backup | -| `build-artifacts/browser` | 5 GiB | `le_app_codex` | TTL 7 days; snapshot artifacts treated as sensitive and never committed | -| `build-artifacts/migration-reports` | 10 GiB | `le_app_codex` | detailed reports TTL 30 days; committed summaries structural only | -| checkout/home metadata | 10 GiB | `le_app_codex` | Git remote is source backup; no personal exports committed | +Authentication remains unresolved. The choices are ChatGPT device authentication or a dedicated API key created specifically for `le_app_codex`. Do not copy Codex configuration, `auth.json`, tokens, SSH keys, Docker configuration, or shell state from `sol6_vi`. -The listed soft budgets total 200 GiB. The 250 GiB hard qgroup ceiling reserves 50 GiB emergency headroom. Startup refuses when host free space is below 100 GiB, project usage exceeds 200 GiB, or required free space for a rehearsal cannot be reserved. The production filesystem is not used for spillover. +Use `CODEX_HOME=/srv/le-app-codex/home/.codex`. Authentication may occur only after the contained user manager, filesystem namespace, network namespace, firewall, resource slice, Codex binary, and acceptance tests are approved. Any `auth.json` must be a nonsymlink regular file owned by `1200:1200`, mode `0600`, and treated as a password. -## Resources intentionally outside `/srv/le-app-codex` +## Repository and migration-source admission -| Resource | Permanent location | Why it remains outside | -|---|---|---| -| Complete untouched Directus archive | `/srv/directus-archive/20260710-175408` | Root-only disaster-recovery evidence; must not enter the Codex trust boundary | -| Production checkout/deployment root | `/var/www/lasereverything.net.db` | Canonical production policy and existing shared-server operations boundary | -| Production Docker image/layer storage | Host runtime-managed location | Operational daemon state, declared by the canonical production deployment but not Git content | -| Production PostgreSQL/media storage | Approved production volume/storage paths | Persistent production data requires independent ownership, capacity, and backup controls | -| Shared public reverse-proxy configuration/certificates | Existing TITANSERVER ingress paths | Server-wide infrastructure serves multiple applications and is never container-controlled | -| Existing self-hosted SMTP infrastructure | Existing mail-server deployment | Production email remains independent; Mailpit is nonproduction-only | +After runtime acceptance and separate approval: -Transitional `/srv/codex-*` paths remain temporarily outside during the rollback period only. They are not permanent alternate project roots and become cleanup candidates solely through the explicit process below. +- create a clean checkout of the reviewed remote `migration/le-app-database` branch at `/srv/le-app-codex/lasereverything.net.db`; +- do not copy `sol6_vi` SSH credentials; the current network design grants the contained environment no Forge SSH exception; +- copy only the credential-scrubbed authoritative source into `database-source-readonly` through an administrator-run, checksum-verified process; +- never use the root archive as migration input; +- generate new nonproduction credentials below `nonproduction-secrets`; never copy production or root-only administration credentials. -## Production ingress boundary +Transitional checkouts, databases, secrets, volumes, and source paths remain intact for at least 30 days and two successful rehearsals. Every later deletion requires separate approval. -Production will use an **internal application-gateway container behind TITANSERVER's existing shared host reverse proxy**. +## Mail and production promotion boundaries -- The server-wide proxy remains host-managed, terminates public TLS, owns ports 80/443, ACME, and cross-application routing. -- The repository-controlled gateway binds only to a dedicated host loopback port or private production network and routes frontend versus Payload internal services. -- The host proxy forwards approved public hostnames to that one gateway and overwrites—not trusts from clients—the forwarding chain and scheme headers. -- The application repository supplies a versioned host-proxy include/template and contract tests, but an operator explicitly reviews/renders/includes it. Containers never mount or edit host proxy configuration or certificates. -- The gateway is not published directly to the internet. Payload admin exposure can be separately restricted at the host proxy. -- Existing `forms.lasereverything.net` behavior remains under the shared ingress until cutover authorization. +Mailpit is nonproduction-only, internal to Compose, has no relay/forwarding, and receives only synthetic or rewritten recipients. Namespace policy rejects direct SMTP on TCP 25/465/587. Production continues to use the existing SMTP service through production-only configuration. -Local development uses its own gateway/TLS because it does not share production ingress. This design preserves server-wide ownership while keeping application routing rules testable and versioned. - -## Transitional resource inventory - -Metadata-only audit results at the planning checkpoint: - -| Resource | Current state | -|---|---| -| `/srv/codex-work/lasereverything.net.db` | Primary Git checkout, branch `temporary/utilities-only`, commit `305ea41`; owns the current local Git database | -| nested `.worktrees/payload-migration` | Migration worktree, branch `migration/le-app-database`; planning commit begins at `ed1c84e` | -| `.migration-source` | Symlink to `/srv/codex-migration/current` | -| `.migration.env` | Symlink to `/srv/codex-secrets/le-payload-migration.env` | -| `/srv/codex-migration/current` | Symlink to `source-20260710-175408` | -| `/srv/codex-migration/source-20260710-175408` | Scrubbed source snapshot, mode `0750`, approximately 150 MB aggregate at audit time | -| `/srv/codex-secrets/le-payload-migration.env` | Current readable nonproduction migration environment, mode `0640`; values not inspected | -| `/srv/codex-secrets/le-payload-migration-admin.env` | Root-only transitional administration environment, mode `0600`; values not inspected and not copied into the development environment | -| `/srv/codex-work` | Approximately 2.0 GB aggregate, including repository history/worktrees and application files | -| recorded rootful MariaDB | container `le-directus-source`, volume `le_directus_source_data_20260710-175408` | -| recorded rootful PostgreSQL | container `le-payload-pg`, volume `le_payload_pg_data_20260710-175408` | -| retired Directus | container `directus`; not a disposable migration resource and not removed by consolidation | - -The current user cannot query the rootful production Docker daemon. Before consolidation, an administrator must produce a metadata-only inventory without printing container environments, labels that contain secrets, database contents, or mount-file contents: - -```bash -sudo docker ps -a --filter name=^/le-directus-source$ \ - --filter name=^/le-payload-pg$ --format '{{.Names}} {{.Image}} {{.Status}}' -sudo docker inspect le-directus-source le-payload-pg \ - --format '{{.Name}} image={{.Image}} mounts={{range .Mounts}}{{.Type}}:{{.Name}}:{{.Destination}};{{end}}' -sudo docker volume inspect \ - le_directus_source_data_20260710-175408 \ - le_payload_pg_data_20260710-175408 \ - --format '{{.Name}} driver={{.Driver}} mountpoint={{.Mountpoint}} labels={{json .Labels}}' -``` - -If names, image IDs, volumes, or mounts differ from the recorded manifest, stop and revise the plan. Do not inspect `.Config.Env` or print secret values. - -## Reversible consolidation procedure - -No step deletes or mutates a transitional resource. Consolidation is copy/recreate/verify, followed by a rollback observation period. - -### Phase A — manifests and Git preservation - -1. Push `migration/le-app-database`, including planning commits, to `origin` and verify the remote commit. -2. Record structural manifests for every transitional path, symlink, owner/mode, aggregate size, Git branch/commit/remote, source checksum manifest, and the administrator container/volume inventory above. -3. Confirm the full root-only archive remains independently present and inaccessible; do not traverse or copy it. -4. Record the exact existing rootful container/volume names and leave them stopped/running exactly as found unless a later approved step says otherwise. - -### Phase B — canonical checkout - -After account/runtime approval, create one clean checkout as the dedicated user: - -```bash -sudo -iu le_app_codex git clone --branch migration/le-app-database --single-branch \ - ssh://FORGE/makearmy/le-app.git \ - /srv/le-app-codex/lasereverything.net.db -sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ - fetch --prune origin -sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ - status --short --branch -sudo -iu le_app_codex git -C /srv/le-app-codex/lasereverything.net.db \ - rev-parse HEAD -``` - -The HEAD must equal the reviewed remote `migration/le-app-database` commit, the worktree must be clean, and no nested permanent worktree may exist. Provision a dedicated repository-scoped Git credential for `le_app_codex`; do not copy `sol6_vi`'s private key. - -### Phase C — scrubbed source copy - -The administrator copies from the resolved source directory, never from the root-only archive: - -```bash -source_old=/srv/codex-migration/source-20260710-175408 -source_new=/srv/le-app-codex/database-source-readonly - -sudo rsync -aHAX --numeric-ids --delete-delay "$source_old/" "$source_new/" - -# Verify supplied manifests without emitting filenames or record contents. -sudo sh -c "cd '$source_new' && sha256sum --check --status CHECKSUMS.sha256" -sudo sh -c "cd '$source_new' && sha256sum --check --status uploads.sha256" - -# Freeze the canonical input against development writes. -sudo chown -R root:le_app_codex "$source_new" -sudo find "$source_new" -type d -exec chmod 0550 {} + -sudo find "$source_new" -type f -exec chmod 0440 {} + -``` - -Before using `--delete-delay`, assert `source_new` resolves exactly to `/srv/le-app-codex/database-source-readonly` and is not a symlink. The source checksum files, row-count manifests, schema, dump, extensions, and all uploads must validate. The old copy remains untouched. - -### Phase D — nonproduction secrets - -- Preserve both old credential files in place during rollback. -- Do not copy `le-payload-migration-admin.env`. -- Generate new fixture/snapshot/rootless database, Payload, Mailpit, and local gateway credentials directly into root-owned `/srv/le-app-codex/nonproduction-secrets` with mode `0640`. -- If the MariaDB read-only credential from the scrubbed environment is temporarily required to compare the old rootful source, transfer only the minimum required variable set through an administrator-reviewed, non-logging process; replace it once the rootless source clone is verified. -- No production SMTP, OAuth, Ko-fi, Directus token, webhook, or server credential enters the new directory. - -### Phase E — rootless database recreation - -1. Restore the scrubbed SQL from canonical read-only `source` into a new project-scoped rootless MariaDB volume. -2. Create a distinct import account with `SELECT` only; application/migration jobs never receive the MariaDB administrative credential. -3. Mount/copy uploads only through finite jobs; the MariaDB and legacy Directus services never receive writable access to canonical `source`. -4. Create fresh disposable rootless PostgreSQL and Payload media volumes. -5. Verify source table counts/checksum summaries against supplied manifests and verify PostgreSQL health/reset isolation. -6. Record new rootless image digests, volume names, Compose project labels, and service health without recording secrets. - -### Phase F — rollback observation period - -Keep `/srv/codex-work`, `/srv/codex-migration`, `/srv/codex-secrets`, rootful `le-directus-source`, rootful `le-payload-pg`, and their recorded volumes intact for at least 30 days **and** through at least two complete successful rootless migration rehearsals, whichever is later. During this period: - -- new work occurs only in the canonical checkout; -- old resources are read-only/frozen where possible; -- checksum, Git, service, and validation comparisons remain reproducible; -- rollback means stop the rootless stack and resume analysis from the untouched transitional resources, not copy changes backward. - -## Later cleanup eligibility — explicit approval required - -Only after the rollback criteria and a fresh inventory may the following become eligible for removal: - -- `/srv/codex-work/lasereverything.net.db/.worktrees/payload-migration` after Git confirms it is clean, pushed, and removable with `git worktree remove`; -- `/srv/codex-work/lasereverything.net.db/.migration-source` and `.migration.env` symlinks; -- `/srv/codex-work/lasereverything.net.db`, then `/srv/codex-work` only if no other entries exist; -- `/srv/codex-migration/current`, `source-20260710-175408`, then `/srv/codex-migration` after canonical checksums pass and rollback expires; -- `/srv/codex-secrets/le-payload-migration.env` and `le-payload-migration-admin.env`, then `/srv/codex-secrets`, after credentials are revoked/obsolete and rollback expires; -- rootful containers `le-directus-source` and `le-payload-pg`; -- rootful volumes `le_directus_source_data_20260710-175408` and `le_payload_pg_data_20260710-175408` after verifying their exact attachment and backup status. - -Every deletion requires a separate explicit approval showing the resolved path/object, before/after checksum or Git proof, attachment status, and rollback consequence. Never include the retired `directus` container, production volumes, `/var/www`, or `/srv/directus-archive/20260710-175408` in this cleanup set. - -## Canonical production promotion - -Promotion path: +Promotion is commit/release based: ```text /srv/le-app-codex/lasereverything.net.db - -> reviewed Git commit + signed/recorded release manifest - -> CI-built immutable image digests tested in fixture/snapshot/rehearsal/staging - -> clean checkout or verified release artifact at /var/www/lasereverything.net.db - -> production deployment invoked only from /var/www/lasereverything.net.db + -> reviewed pushed commit and immutable image/release manifest + -> fixture -> snapshot -> rehearsal -> staging + -> clean checkout or verified release artifact + -> /var/www/lasereverything.net.db ``` -Required workflow: +Never recursively copy or `rsync` the nonproduction workspace into production. Codex stops for explicit approval before changing `/var/www`, public proxy/DNS/TLS, production SMTP, production databases, Directus, or transitional/archive resources. -1. The canonical nonproduction checkout is clean and all accepted commits are pushed. -2. CI builds images once, records digests/SBOM/provenance, and runs the accepted test gates. -3. Staging deploys those exact digests from the same Compose core and an approved staging overlay. -4. A reviewed release manifest pins Git commit, image digests, migration set, configuration schema, health contract, and rollback compatibility. -5. On TITANSERVER, an operator updates a clean Git checkout at `/var/www/lasereverything.net.db` to the reviewed commit (or atomically unpacks a verified release artifact into that exact root), verifies a clean tree, and verifies the release manifest. -6. From that directory only, deployment scripts render/validate production Compose and the host-ingress include, run the gated schema job, pull pinned digests, and roll services with health checks. +## Unresolved decisions checklist -The production checkout contains all application source/configuration needed to reproduce the deployment contract. Physical image layers, declared production PostgreSQL/media volumes, and shared host-proxy configuration may remain outside it, but their names/locations/interfaces are declared in the production overlay and release manifest rooted there. +- Phase 2 package path: scheduled full host maintenance or clean-chroot/current-system-compatible investigation. +- Approved DNS resolver IPv4 addresses; `192.168.10.1` is currently observed but not approved. +- Complete current host, public/NAT, LAN, management, VPN, WireGuard, Docker bridge, and other container-network inventory. +- Codex authentication: device authentication or dedicated API key. +- Disk-growth enforcement: Btrfs quota policy or another separately approved mechanism. +- Whether deferred process/hostname hardening options pass acceptance tests. +- Any individually approved future host-to-namespace published endpoint; none is currently allowed. -Forbidden promotion inputs include `/srv/le-app-codex/container-runtime`, `database-source-readonly`, `nonproduction-secrets`, `testing-data`, `build-artifacts`, local CA material, Mailpit state, test traces, rootless Docker state, disposable databases/volumes, caches, and development `.env` files. No `rsync` or recursive copy from `/srv/le-app-codex` to `/var/www` is permitted. - -## Local HTTPS and CA trust - -The local gateway uses a repository-pinned proxy image and an ignored, generated development CA to serve: - -- `app.le.localhost`; -- `cms.le.localhost`; -- `mail.le.localhost`; -- optional `legacy.le.localhost`. - -All bind to loopback port 8443, avoiding privileged ports and host DNS changes (`*.localhost` resolves locally). Certificates and private keys live under `/srv/le-app-codex/nonproduction-secrets/local-ca`, mode `0700/0600`, excluded from Git and build contexts. - -Automated Playwright tests may use a dedicated browser profile with its own trust database. Manual Chromium trust can be limited to `/srv/le-app-codex/home/.pki/nssdb` using the already installed `certutil`; system-wide CA trust is unnecessary. Importing the CA into even that user/browser profile is a host/user-state change and requires explicit approval. Removal deletes the certificate from that profile and the ignored CA directory. - -## Mailpit and outbound protection - -Mailpit exists only in fixture, snapshot, rehearsal, and default staging modes: - -- internal Compose networks only; UI reaches the browser through the local gateway; -- no public host bind and no production ingress route; -- SMTP relay/forwarding and webhook forwarding disabled; -- no production SMTP variables mounted; -- tests query Mailpit's internal HTTP API and follow local HTTPS verification/recovery links; -- registration, verification, recovery, supporter claim, and notification paths are covered. - -Defense in depth: - -1. Mode-specific environment schema rejects production SMTP hostnames/sender domains, ports 25/465/587 as external endpoints, production credential variable scopes, OAuth secrets, Ko-fi production tokens, and webhook credentials. -2. Runtime services attach only to an `internal: true` application network in nonproduction. Mail is addressed to the internal `mailpit:1025` service. -3. Snapshot-derived recipient addresses are rejected or rewritten at the transport boundary to deterministic reserved-domain aliases. Only synthetic fixture accounts may be addressed directly. -4. No production credential exists in rendered Compose, container environment, secret mounts, or image history. -5. A finite `egress-test` runs from the exact frontend and Payload runtime network namespaces: Mailpit delivery must succeed; connection attempts to controlled external test endpoints on SMTP ports must fail; DNS/HTTP behavior is recorded separately. - -Firewall-level isolation will not be claimed until these tests pass under the selected rootless runtime. If Docker `internal` networking still permits an undesired host/external route, the implementation stops and proposes an approved rootless egress control rather than asserting safety. Tests use an administrator-approved controlled endpoint, never a real third-party SMTP server. - -Production remains unchanged: application services use the existing self-hosted SMTP infrastructure and real recipients through production-only secrets and network policy. Mailpit never replaces or modifies it. - -## BGBye CPU/GPU profiles - -Milestone 0 starts with a deterministic CPU-compatible BGBye image/profile: - -- same FastAPI paths, methods endpoint shape, request fields, status codes, content types, health/readiness schema, and error contract as GPU; -- one pinned CPU-capable segmentation model for real tiny-image processing in routine tests; -- image upload/proxy limits, invalid method/file handling, alpha PNG output, concurrency serialization, temporary-file lifecycle, status polling, and FFmpeg contract can be exercised; -- model artifacts are pinned/checksummed and readiness fails until loaded. - -CUDA-only validation is deferred to an opt-in GPU profile/staging gate: - -- NVIDIA runtime/device visibility and driver compatibility; -- every production model/backend, CUDA placement, GPU lock/concurrency, memory cleanup/OOM behavior, performance envelope, and longer video processing; -- production model-cache checksum and offline startup behavior. - -CPU and GPU images share server contract tests. Capability differences appear in explicit health/method metadata rather than changing endpoints. A production GPU image is released only after model preload/readiness, representative image/video output, cleanup, restart, and resource tests pass on GPU staging. Rootless GPU provisioning does not block the first stack. - -## Rootless runtime rollback/removal - -Rollback must be run only after confirming the selected context/socket is the dedicated one: - -```bash -# As le_app_codex: stop project resources, then the user daemon. -DOCKER_HOST=unix:///run/user/1200/docker.sock docker compose \ - --project-name le-app-testing down --remove-orphans -systemctl --user disable --now docker.socket docker.service - -# As administrator: disable optional lingering. -sudo loginctl disable-linger le_app_codex - -# Verify production daemon endpoint and data were never referenced by project metadata/logs. -# Use the planning preflight/audit script; do not connect to or mutate production Docker. - -# Remove only the dedicated project subvolume after an explicit artifact review. -sudo btrfs subvolume delete /srv/le-app-codex - -# Remove subordinate allocations and account after the subvolume/runtime is gone. -sudo usermod --del-subuids 165536-231071 --del-subgids 165536-231071 le_app_codex -sudo userdel --remove le_app_codex -sudo groupdel le_app_codex 2>/dev/null || true -``` - -The dedicated Docker context lives only in the dedicated user's home and disappears with that home. If another operator created a client context pointing to the user socket, remove that context explicitly from that operator's client configuration. - -Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Any temporary transitional ACLs, if an administrator added them contrary to the final copy-based design, must be inventoried and removed before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, transitional source during its rollback retention, or `/srv/directus-archive`. - -Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-app-codex` resources were deleted. +Until these decisions are resolved, do not proceed beyond documentation and read-only verification. diff --git a/docs/le-app-database-migration/implementation-plan.md b/docs/le-app-database-migration/implementation-plan.md index 67c90af..7e113e6 100644 --- a/docs/le-app-database-migration/implementation-plan.md +++ b/docs/le-app-database-migration/implementation-plan.md @@ -9,13 +9,16 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt - Review `architecture-proposal.md`, `environment-parity.md`, and host prerequisites. 2. **Host runtime gate — requires human approval/provisioning** - - Provision the dedicated `le_app_codex` account and a rootless Docker context owned only by it. + - Preserve the completed inert `le_app_codex` Phase 1 identity/workspace; do not rerun bootstrap artifacts. + - Select the Phase 2 package path separately; never perform a partial Arch upgrade or install the unmodified AUR extras package. + - Use a version-matched, checksum-pinned Moby launcher and a genuine per-user Docker service managed through root-controlled `user@1200.service`; never enable lingering or run `dockerd` from a system service with `User=le_app_codex`. + - Apply containment and the proposed `user-1200.slice` limits to the complete user manager; accept only `unix:///run/user/1200/docker.sock`. + - Create an IPv4-only root-owned namespace with default-deny egress and explicit host/LAN/WireGuard/metadata/private/loopback/bridge/SMTP/IPv6 denial after DNS and complete address inventories are approved. - Restrict production environment files currently readable to `sol6_vi`; verify the development account cannot traverse production paths. - Use deterministic CPU BGBye initially; defer optional rootless GPU provisioning. - Approve browser-profile-scoped local CA trust for HTTPS testing. - - Confirm nonproduction storage/capacity locations. - - Verify forbidden production paths and sockets are inaccessible. - - Execute every positive and negative preflight in `host-runtime-plan.md` before application work. + - Resolve authentication and disk-quota policy separately; confirm nonproduction storage/capacity locations. + - Execute every inert positive and negative preflight in `host-runtime-plan.md` before starting the user manager, Docker, Codex, or application work. 3. **Reversible canonical-path consolidation — separately approval-gated** - Inventory the transitional Git roots, symlinks, credential filenames, rootful disposable containers, and volumes without reading secret values. From 2cf249d6f258e9fa80fcb683f893eff6d4ec3556 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 16:10:24 -0400 Subject: [PATCH 07/14] add Phase 2A host maintenance procedure --- .../host-runtime-plan.md | 6 +- .../implementation-plan.md | 2 +- .../phase2a-host-maintenance/MANIFEST.sha256 | 6 + .../phase2a-host-maintenance/README.md | 121 +++++++ .../execute-maintenance.sh | 316 +++++++++++++++++ .../post-reboot-validate.sh | 222 ++++++++++++ .../preflight-read-only.sh | 325 ++++++++++++++++++ .../reboot-procedure.md | 65 ++++ .../rollback-recovery.md | 59 ++++ 9 files changed, 1119 insertions(+), 3 deletions(-) create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/README.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/reboot-procedure.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/rollback-recovery.md diff --git a/docs/le-app-database-migration/host-runtime-plan.md b/docs/le-app-database-migration/host-runtime-plan.md index f01bd29..b33516f 100644 --- a/docs/le-app-database-migration/host-runtime-plan.md +++ b/docs/le-app-database-migration/host-runtime-plan.md @@ -52,11 +52,13 @@ pacman -Syu --needed rootlesskit slirp4netns openai-codex was a 221-package full host upgrade affecting both installed kernels and OpenSSH. It is a host-maintenance operation requiring a fresh exact transaction preview, reboot planning, service-health checks, `.pacnew` review, and separate operator approval. A partial Arch upgrade is not acceptable. -The unresolved package paths are: +The package paths are: 1. schedule and approve the complete refreshed host upgrade; or 2. investigate clean-chroot, checksum-pinned packages compatible with the current system, without partially upgrading repository packages. +The operator provisionally selected Path A on 2026-07-12 as the preferred Phase 2 package path because it is the shortest safe route consistent with Arch's complete-upgrade model. This records planning direction only: the transaction, maintenance window, package execution, and reboot remain separately approval-gated. The uncommitted review package is under `docs/le-app-database-migration/phase2a-host-maintenance/`. + Do not install the unmodified AUR `docker-rootless-extras` package. Its install hook reloads global sysctls, which exceeds this project's boundary. Do not curl an installer into a shell. Install only a reviewed Moby `contrib/dockerd-rootless.sh` matching the Docker engine actually installed after the chosen package path. At the current checkpoint Docker is `29.6.1`; the candidate is: @@ -311,7 +313,7 @@ Never recursively copy or `rsync` the nonproduction workspace into production. C ## Unresolved decisions checklist -- Phase 2 package path: scheduled full host maintenance or clean-chroot/current-system-compatible investigation. +- Phase 2 package path: Path A is provisionally preferred; package execution and reboot are not approved. - Approved DNS resolver IPv4 addresses; `192.168.10.1` is currently observed but not approved. - Complete current host, public/NAT, LAN, management, VPN, WireGuard, Docker bridge, and other container-network inventory. - Codex authentication: device authentication or dedicated API key. diff --git a/docs/le-app-database-migration/implementation-plan.md b/docs/le-app-database-migration/implementation-plan.md index 7e113e6..93e3583 100644 --- a/docs/le-app-database-migration/implementation-plan.md +++ b/docs/le-app-database-migration/implementation-plan.md @@ -10,7 +10,7 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt 2. **Host runtime gate — requires human approval/provisioning** - Preserve the completed inert `le_app_codex` Phase 1 identity/workspace; do not rerun bootstrap artifacts. - - Select the Phase 2 package path separately; never perform a partial Arch upgrade or install the unmodified AUR extras package. + - Preserve the provisional Path A selection: prepare a scheduled complete Arch host upgrade, but require separate approval for the final transaction and reboot; never perform a partial Arch upgrade or install the unmodified AUR extras package. - Use a version-matched, checksum-pinned Moby launcher and a genuine per-user Docker service managed through root-controlled `user@1200.service`; never enable lingering or run `dockerd` from a system service with `User=le_app_codex`. - Apply containment and the proposed `user-1200.slice` limits to the complete user manager; accept only `unix:///run/user/1200/docker.sock`. - Create an IPv4-only root-owned namespace with default-deny egress and explicit host/LAN/WireGuard/metadata/private/loopback/bridge/SMTP/IPv6 denial after DNS and complete address inventories are approved. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 new file mode 100644 index 0000000..486cbbd --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 @@ -0,0 +1,6 @@ +756dfad7eddf89406383804f891266bd814c6ca974a5a250fddc72b67c055ddb README.md +9d9db1819ef473954ea9f5009dc985f3b8b090406246bb22a10ffa09caf3e692 preflight-read-only.sh +ab7220b2042b6a256c26189b37e66637cc7cf741420265ecefd94ab6fa6c06b7 execute-maintenance.sh +5b88c68da055d3578e8b19fdf7623c3de204d5ef0af4b17e113938d8bd0a2c5e reboot-procedure.md +b6a67a01d4c4f786906d98603ff3075deff29fb6bc4483a642f18a187e4f552a post-reboot-validate.sh +0805397870c66a523f693cfdba33f7b08c94793b62ddadf720ff84c22796ea13 rollback-recovery.md diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/README.md new file mode 100644 index 0000000..c6b4cf5 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/README.md @@ -0,0 +1,121 @@ +# Phase 2A scheduled host-maintenance review package + +Status: **review-only and uncommitted**. Operator provisionally prefers Path A, a scheduled complete Arch host upgrade. This is not execution approval. + +Proposed package command: + +```bash +pacman -Syu --needed rootlesskit slirp4netns openai-codex +``` + +This package deliberately separates: + +1. read-only preflight: `preflight-read-only.sh`; +2. package execution: `execute-maintenance.sh` — generated, never run during preparation; +3. reboot: `reboot-procedure.md` — separate approval and manual command; +4. post-reboot validation: `post-reboot-validate.sh`; +5. later contained rootless-Docker provisioning — explicitly outside Phase 2A. + +The scripts do not enable lingering, configure rootless Docker, modify systemd or nftables, authenticate Codex, copy data, or change production configuration. Package hooks may restart services during the approved upgrade; the execution script records this but does not add its own service restarts. + +## Required operator inputs + +Before execution, record site-specific values in the maintenance ticket: + +- independent console/recovery method and a tested root login path; +- public health URLs, including the canonical application utility endpoint; +- production-critical unit names beyond the conservative defaults in the scripts; +- expected running Docker container names and health states; +- bootloader type and known-good previous kernel entry; +- backup identifiers and restoration instructions; +- maintenance window, user notification, and rollback decision owner. + +URLs can be supplied without editing the scripts: + +```bash +export PUBLIC_HEALTH_URLS='https://example.invalid/health https://example.invalid/' +``` + +Health URLs must be non-secret public HTTPS URLs. The scripts reject user-info credentials, query strings, fragments, whitespace/control characters, and non-HTTPS schemes. Never place tokens, signed parameters, credentials, or private endpoints in `PUBLIC_HEALTH_URLS`; the scripts do not read environment files to discover URLs and do not log redirected effective URLs. + +Unit names can be extended similarly: + +```bash +export EXTRA_CRITICAL_UNITS='example.service another.service' +``` + +The operator must separately verify independent recovery and restorable backups, then set the non-secret attestations: + +```bash +export LE_PHASE2A_RECOVERY_READY=YES +export LE_PHASE2A_BACKUPS_READY=YES +``` + +## Invocation and enforced records + +Invoke every script as a separate process, never with `source` or `.`. Explicitly capture its return status: + +```bash +sudo --preserve-env=PUBLIC_HEALTH_URLS,EXTRA_CRITICAL_UNITS,LE_PHASE2A_RECOVERY_READY,LE_PHASE2A_BACKUPS_READY \ + bash docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh +preflight_rc=$? +printf 'preflight_rc=%s\n' "$preflight_rc" +``` + +The preflight creates an atomic unique root-owned mode `0700` directory below `/var/log/le-phase2a-preflight`, prints its path, and returns nonzero on `HARD_STOP`. Its machine-readable `result.manifest` records hostname, boot ID, kernel, timestamp, result, failures, and inventory hashes. Maintenance accepts only a checksum-valid `PASS` record from the same host and boot that is no more than **1,800 seconds (30 minutes)** old: + +```bash +sudo bash docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh \ + --preflight /var/log/le-phase2a-preflight/preflight.YYYYMMDDTHHMMSSZ.XXXXXXXX +maintenance_rc=$? +printf 'maintenance_rc=%s\n' "$maintenance_rc" +``` + +Maintenance and post-reboot validation create their own atomic unique root-only records below `/var/log/le-phase2a-maintenance` and `/var/log/le-phase2a-post-reboot`. Post-reboot invocation requires the exact maintenance record: + +```bash +sudo bash docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh \ + /var/log/le-phase2a-maintenance/maintenance.YYYYMMDDTHHMMSSZ.XXXXXXXX +validation_rc=$? +printf 'validation_rc=%s\n' "$validation_rc" +``` + +These records can contain IP addresses, usernames, unit/container names, package state, and limited relevant journal metadata. They never intentionally capture process/container environments, environment-file contents, private keys, credentials, secret files, or command history. Treat every record as operationally sensitive: keep it root-only, do not commit it, and disclose it only through the approved maintenance channel. + +## Transaction comparison limitation + +The maintenance script refreshes an isolated sync database, normalizes package name, old version, new version, action, count, and downgrade status, and requires exact equality with the approved retained preview. Any detectable mismatch stops execution. + +The final live `pacman -Syu` must refresh the live sync databases as part of the complete transaction. Pacman cannot expose that post-refresh final interactive transaction for wrapper comparison without either staging a live `pacman -Sy`-only state or replacing the transaction with an unsafe custom workflow. The script therefore requires two exact operator phrases before launch and then requires the operator to review Pacman's final package table at its native interactive `Y/n` prompt. Answer `n` on any difference. This limitation is explicit and is not represented as machine-enforced equality. + +## Package-preview evidence + +The retained preview is: + +```text +/tmp/le-phase2-package-decision-20260712.Mn7mIx +``` + +Expected report checksum: + +```text +195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d phase2-package-decision-report.md +``` + +The directory occupies approximately 2.4 GiB. Do not delete it yet. It becomes cleanup-eligible only after the operator accepts the maintenance record, the upgrade/reboot succeeds, and retained evidence has been copied to durable approved storage if required. Cleanup is a later explicit command and is not included in any script. + +## Hard stop conditions + +Stop rather than improvise if any of the following occurs: + +- no independent recovery access or root path; +- insufficient space or an unhealthy `/boot`; +- degraded storage, failed backup, unresolved failed units, or unhealthy production service; +- unexpected package count/version, downgrade, repository error, signature error, keyring error, mirror inconsistency, or partial transaction; +- SSH access is unstable before the OpenSSH restart hook; +- DKMS/initramfs/kernel/bootloader generation fails; +- package database inconsistency or unexplained `.pacnew` affecting boot, SSH, PAM, networking, PHP, database, mail, proxy, or firewall configuration; +- production containers or public endpoints fail after the transaction; +- the operator cannot identify a known-good previous kernel and recovery route. + +Do not proceed from package maintenance into rootless-Docker provisioning. That later work remains separately approval-gated by `host-runtime-plan.md`. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh new file mode 100644 index 0000000..6e89195 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh @@ -0,0 +1,316 @@ +#!/usr/bin/env bash +# GENERATED FOR REVIEW. Run only as a separate process after explicit approval. +# No shell-wide error options and no parent-shell termination commands. + +fail() { + printf 'HARD_STOP: %s\n' "$1" >&2 + return "${2:-1}" +} + +manifest_value() { + key=$1 + file=$2 + value=$(awk -F= -v k="$key" '$1==k {sub(/^[^=]*=/, ""); print; found++} END {if(found!=1) exit 1}' "$file") + rc=$? + if [ "$rc" -ne 0 ]; then return "$rc"; fi + printf '%s\n' "$value" + return 0 +} + +validate_preflight() { + preflight=$1 + if [ -z "$preflight" ] || [ ! -d "$preflight" ] || [ -L "$preflight" ]; then + fail 'preflight path is missing, not a directory, or a symlink' 30 + return $? + fi + owner=$(stat -c %u "$preflight" 2>/dev/null) + mode=$(stat -c %a "$preflight" 2>/dev/null) + if [ "$owner" != 0 ] || [ "$mode" != 700 ]; then + fail 'preflight record must be root-owned mode 0700' 31 + return $? + fi + manifest=$preflight/result.manifest + if [ ! -f "$manifest" ] || [ -L "$manifest" ] || [ ! -f "$preflight/result.manifest.sha256" ]; then + fail 'preflight result manifest is missing or a symlink' 32 + return $? + fi + (cd "$preflight" && sha256sum -c result.manifest.sha256 > /dev/null 2>&1) + rc=$? + if [ "$rc" -ne 0 ]; then fail 'preflight result manifest checksum failed' 32; return $?; fi + format=$(manifest_value format "$manifest") || { fail 'malformed preflight format' 33; return $?; } + recorded_host=$(manifest_value hostname "$manifest") || { fail 'malformed preflight hostname' 33; return $?; } + recorded_boot=$(manifest_value boot_id "$manifest") || { fail 'malformed preflight boot ID' 33; return $?; } + recorded_kernel=$(manifest_value running_kernel "$manifest") || { fail 'malformed preflight kernel' 33; return $?; } + recorded_time=$(manifest_value timestamp_epoch "$manifest") || { fail 'malformed preflight timestamp' 33; return $?; } + recorded_overall=$(manifest_value overall "$manifest") || { fail 'malformed preflight result' 33; return $?; } + recorded_failure_count=$(manifest_value failure_count "$manifest") || { fail 'malformed preflight failure count' 33; return $?; } + recorded_failed_checks_hash=$(manifest_value failed_checks_sha256 "$manifest") || { fail 'malformed failed-check hash' 33; return $?; } + recorded_hash=$(manifest_value inventory_hashes_sha256 "$manifest") || { fail 'malformed inventory hash' 33; return $?; } + [ "$format" = le-phase2a-preflight-v1 ] || { fail 'unsupported preflight format' 34; return $?; } + [ "$recorded_overall" = PASS ] || { fail 'preflight result is not PASS' 35; return $?; } + [ "$recorded_failure_count" = 0 ] || { fail 'preflight records mandatory failures' 35; return $?; } + if [ ! -f "$preflight/failed-mandatory-checks.txt" ] || [ -L "$preflight/failed-mandatory-checks.txt" ]; then + fail 'failed-check list is missing or a symlink' 35 + return $? + fi + actual_failed_checks_hash=$(sha256sum "$preflight/failed-mandatory-checks.txt" 2>/dev/null | awk '{print $1}') + [ "$actual_failed_checks_hash" = "$recorded_failed_checks_hash" ] || { fail 'failed-check list checksum mismatch' 35; return $?; } + [ "$recorded_host" = "$(hostname)" ] || { fail 'preflight belongs to another host' 36; return $?; } + [ "$recorded_boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] || { fail 'preflight belongs to another boot' 37; return $?; } + [ "$recorded_kernel" = "$(uname -r)" ] || { fail 'running kernel changed since preflight' 37; return $?; } + case "$recorded_time" in *[!0-9]*|'') fail 'preflight timestamp is invalid' 38; return $? ;; esac + now=$(date +%s) + age=$((now - recorded_time)) + if [ "$age" -lt 0 ] || [ "$age" -gt 1800 ]; then + fail "preflight is stale; age=${age}s limit=1800s" 39 + return $? + fi + actual_hash=$(sha256sum "$preflight/inventory-hashes.sha256" 2>/dev/null | awk '{print $1}') + [ "$actual_hash" = "$recorded_hash" ] || { fail 'preflight inventory-hash manifest checksum mismatch' 40; return $?; } + (cd "$preflight" && sha256sum -c inventory-hashes.sha256 > /dev/null 2>&1) + rc=$? + if [ "$rc" -ne 0 ]; then + fail 'one or more preflight inventory files failed checksum validation' 41 + return $? + fi + validated_preflight=$preflight + return 0 +} + +validate_health_url_file() { + file=$1 + [ -s "$file" ] || { fail 'validated preflight contains no health URLs' 42; return $?; } + while IFS= read -r url; do + case "$url" in https://*) ;; *) fail 'unsupported health URL scheme' 42; return $? ;; esac + authority=${url#https://}; authority=${authority%%/*} + case "$url" in *'?'*|*'#'*|*$'\n'*|*$'\r'*|*$'\t'*|*' '*) fail 'unsafe health URL content' 42; return $? ;; esac + case "$authority" in ''|*@*) fail 'health URL user-info or empty authority rejected' 42; return $? ;; esac + done < "$file" + return 0 +} + +rerun_volatile_checks() { + systemctl --failed --no-legend --plain --no-pager > "$record/volatile-failed-units.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot capture current failed units' "$rc"; return $?; } + [ ! -s "$record/volatile-failed-units.txt" ] || { fail 'one or more failed units are present' 45; return $?; } + pacman -Dk > "$record/volatile-pacman-Dk.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'package database consistency failed immediately before execution' "$rc"; return $?; } + df -hT / /boot /var/cache/pacman/pkg /tmp > "$record/volatile-disk-space.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot verify current disk space' "$rc"; return $?; } + while IFS= read -r container; do + [ -n "$container" ] || continue + running=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null) + printf '%s|running=%s|health=%s\n' "$container" "${running:-missing}" "${health:-unknown}" \ + >> "$record/volatile-container-health.txt" + if [ "$running" != true ] || [ "$health" = unhealthy ]; then + fail "previously running container is unavailable: $container" 43 + return $? + fi + done < "$validated_preflight/running-containers-before.txt" + : > "$record/volatile-public-health.txt" + while IFS= read -r url; do + metrics=$(curl --fail --silent --show-error --max-redirs 0 \ + --connect-timeout 10 --max-time 30 --output /dev/null \ + --write-out 'http=%{http_code}|remote=%{remote_ip}|time=%{time_total}' \ + "$url" 2>> "$record/volatile-public-health-errors.txt") + rc=$? + printf '%s|%s\n' "$url" "$metrics" >> "$record/volatile-public-health.txt" + if [ "$rc" -ne 0 ]; then fail "public health failed immediately before execution: $url" 44; return $?; fi + done < "$validated_preflight/health-urls.sanitized" + return 0 +} + +normalize_transaction() { + source_file=$1 + output_file=$2 + downgrade_file=$3 + : > "$output_file" + : > "$downgrade_file" + while IFS='|' read -r name new_version repository location download_bytes; do + [ -n "$name" ] || continue + old_line=$(pacman -Q "$name" 2>/dev/null) + if [ -n "$old_line" ]; then + old_version=${old_line#* } + comparison=$(vercmp "$new_version" "$old_version") + if [ "$comparison" -lt 0 ]; then action=downgrade + elif [ "$comparison" -eq 0 ]; then action=same + else action=upgrade + fi + else + old_version=- + action=new + fi + printf '%s|%s|%s|%s\n' "$name" "$old_version" "$new_version" "$action" >> "$output_file" + if [ "$action" = downgrade ]; then + printf '%s|%s|%s|%s\n' "$name" "$old_version" "$new_version" "$action" >> "$downgrade_file" + fi + done < "$source_file" + sort -o "$output_file" "$output_file" + return 0 +} + +main() { + if [ "$(id -u)" -ne 0 ]; then fail 'run as a separate root process' 10; return $?; fi + if [ "$1" != --preflight ] || [ -z "${2:-}" ] || [ -n "${3:-}" ]; then + fail 'usage: execute-maintenance.sh --preflight /var/log/le-phase2a-preflight/preflight.XXXXXXXX' 11 + return $? + fi + validate_preflight "$2" || return $? + validate_health_url_file "$validated_preflight/health-urls.sanitized" || return $? + + umask 077 + install -d -o root -g root -m 0700 /var/log/le-phase2a-maintenance + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot create root-only maintenance record parent' "$rc"; return $?; } + record=$(mktemp -d "/var/log/le-phase2a-maintenance/maintenance.$(date -u +%Y%m%dT%H%M%SZ).XXXXXXXX") + rc=$? + if [ "$rc" -ne 0 ] || [ -z "$record" ] || [ ! -d "$record" ]; then + fail 'cannot atomically create unique maintenance record' 12 + return $? + fi + chown root:root "$record"; chmod 0700 "$record" + maintenance_start=$(date -u +%FT%TZ) + printf '%s\n' "$record" > "$record/RECORD_PATH.txt" + cp -a "$validated_preflight" "$record/preflight-record" + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot copy validated preflight record' "$rc"; return $?; } + + report=/tmp/le-phase2-package-decision-20260712.Mn7mIx/phase2-package-decision-report.md + report_manifest=/tmp/le-phase2-package-decision-20260712.Mn7mIx/SHA256SUMS.main + printf '%s %s\n' 195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d "$report" | sha256sum -c - \ + > "$record/approved-report-checksum.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'approved report checksum failed' "$rc"; return $?; } + cp "$report" "$report_manifest" "$record/" + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot copy approved report and checksum manifest' "$rc"; return $?; } + + uname -a > "$record/running-kernel-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture running kernel' "$rc"; return $?; } + cat /proc/sys/kernel/random/boot_id > "$record/boot-id-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture boot ID' "$rc"; return $?; } + pacman -Q > "$record/packages-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture installed package inventory' "$rc"; return $?; } + cp "$validated_preflight/bootloader-inventory.txt" "$validated_preflight/kernel-initramfs-before.txt" \ + "$validated_preflight/running-containers-before.txt" "$validated_preflight/containers-all.psv" \ + "$validated_preflight/critical-units-baseline.psv" "$validated_preflight/database-baseline.psv" "$record/" + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot copy essential rollback baseline evidence' "$rc"; return $?; } + systemctl --failed --no-legend --plain --no-pager > "$record/failed-units-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture failed-unit baseline' "$rc"; return $?; } + [ ! -s "$record/failed-units-before.txt" ] || { fail 'failed units appeared before transaction preview' 45; return $?; } + + rerun_volatile_checks || return $? + + preview_root=$(mktemp -d /tmp/le-phase2a-execution-preview.XXXXXXXX) + rc=$? + if [ "$rc" -ne 0 ] || [ -z "$preview_root" ] || [ ! -d "$preview_root" ]; then + fail 'cannot create isolated transaction preview directory' 13 + return $? + fi + chmod 0700 "$preview_root"; mkdir -m 0700 "$preview_root/db" "$preview_root/cache" + ln -s /var/lib/pacman/local "$preview_root/db/local" + + fakeroot -- pacman --config /etc/pacman.conf --dbpath "$preview_root/db" \ + --cachedir "$preview_root/cache" --logfile "$preview_root/pacman-preview.log" -Sy \ + > "$record/isolated-sync-refresh.log" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'isolated repository refresh failed; live databases untouched' "$rc"; return $?; } + fakeroot -- pacman --config /etc/pacman.conf --dbpath "$preview_root/db" \ + --cachedir "$preview_root/cache" --logfile "$preview_root/pacman-preview.log" \ + -Su --needed --print-format '%n|%v|%r|%l|%s' rootlesskit slirp4netns openai-codex \ + > "$record/latest-transaction.psv" 2> "$record/latest-transaction.err" + rc=$? + [ "$rc" -eq 0 ] || { fail 'latest complete transaction resolution failed' "$rc"; return $?; } + normalize_transaction "$record/latest-transaction.psv" "$record/latest-normalized-actions.psv" "$record/latest-downgrades.psv" + [ ! -s "$record/latest-downgrades.psv" ] || { fail 'latest transaction contains a downgrade' 14; return $?; } + + awk -F'|' '{print $1"|"$2"|"$3"|"$5}' \ + /tmp/le-phase2-package-decision-20260712.Mn7mIx/inventory.psv | sort \ + > "$record/approved-normalized-actions.psv" + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot normalize approved preview' "$rc"; return $?; } + diff -u "$record/approved-normalized-actions.psv" "$record/latest-normalized-actions.psv" \ + > "$record/approved-vs-latest.diff" 2>&1 + diff_rc=$? + if [ "$diff_rc" -ne 0 ]; then + fail 'latest package names, versions, or actions differ from the approved preview' 15 + return $? + fi + latest_count=$(wc -l < "$record/latest-normalized-actions.psv") + approved_count=$(wc -l < "$record/approved-normalized-actions.psv") + [ "$latest_count" = "$approved_count" ] || { fail 'transaction package counts differ' 15; return $?; } + for required in rootlesskit slirp4netns openai-codex; do + grep -q "^${required}|" "$record/latest-normalized-actions.psv" + rc=$?; [ "$rc" -eq 0 ] || { fail "latest transaction omits $required" 16; return $?; } + done + + cp "$record/latest-transaction.psv" "$record/latest-normalized-actions.psv" \ + "$record/approved-normalized-actions.psv" "$record/approved-vs-latest.diff" "$record/transaction-for-approval.psv" + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot preserve transaction preview evidence' "$rc"; return $?; } + sha256sum "$record/latest-transaction.psv" "$record/latest-normalized-actions.psv" \ + "$record/approved-normalized-actions.psv" > "$record/transaction-preview-hashes.sha256" + + printf '\nMachine comparison passed: packages=%s, no downgrades, exact match to approved preview.\n' "$latest_count" + cat "$record/latest-normalized-actions.psv" + printf '\nType exactly: APPROVE MATCHED COMPLETE HOST UPGRADE\n> ' + IFS= read -r approval_one + if [ "$approval_one" != 'APPROVE MATCHED COMPLETE HOST UPGRADE' ]; then + fail 'first exact operator approval not granted' 20 + return $? + fi + + rerun_volatile_checks || return $? + printf '\nLIMITATION: the final live -Syu refresh may change after this isolated comparison.\n' + printf 'Pacman will display its final interactive transaction and ask Y/n. The wrapper cannot machine-compare that post-refresh transaction without an unsafe live -Sy-only staging step.\n' + printf 'Type exactly: ACKNOWLEDGE FINAL PACMAN TRANSACTION REQUIRES MANUAL REVIEW\n> ' + IFS= read -r approval_two + if [ "$approval_two" != 'ACKNOWLEDGE FINAL PACMAN TRANSACTION REQUIRES MANUAL REVIEW' ]; then + fail 'second exact operator acknowledgement not granted' 21 + return $? + fi + + printf 'At Pacman Y/n, answer no if any package, version, action, count, downgrade status, signature, or repository differs.\n' + script -q -e -f -c 'pacman -Syu --needed rootlesskit slirp4netns openai-codex' "$record/pacman-complete.log" + rc=$? + if [ "$rc" -ne 0 ]; then fail "Pacman returned $rc; stop without ad hoc repair" "$rc"; return $?; fi + + pacman -Q > "$record/packages-after.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture post-transaction package inventory' "$rc"; return $?; } + diff -u "$record/packages-before.txt" "$record/packages-after.txt" > "$record/package-inventory.diff" 2>&1 + diff_rc=$?; [ "$diff_rc" -le 1 ] || { fail 'package inventory comparison failed' "$diff_rc"; return $?; } + find /etc -xdev -type f \( -name '*.pacnew' -o -name '*.pacsave' \) -printf '%p\n' | sort \ + > "$record/pacnew-pacsave-after.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot inventory pacnew/pacsave files' "$rc"; return $?; } + pacdiff -o > "$record/pacdiff-outstanding.txt" 2>&1 + pacdiff_rc=$? + if [ "$pacdiff_rc" -ne 0 ] && [ "$pacdiff_rc" -ne 1 ]; then + printf 'pacdiff-observation|rc=%s\n' "$pacdiff_rc" > "$record/warnings-after.txt" + fi + grep -E -i 'hook|restart|reload|dkms|dracut|initramfs|mkinitcpio|grub|pacnew|pacsave|warning|error|failed' \ + "$record/pacman-complete.log" > "$record/hook-restart-summary.txt" 2>&1 + systemctl --failed --no-legend --plain --no-pager > "$record/failed-units-after.txt" 2>&1 + journal_units='sshd.service accounts-daemon.service docker.service' + while IFS='|' read -r unit _; do journal_units="$journal_units $unit"; done < "$record/critical-units-baseline.psv" + journalctl --since "$maintenance_start" --no-pager $(for unit in $journal_units; do printf -- '-u %q ' "$unit"; done) \ + > "$record/relevant-service-journal.txt" 2>&1 + dkms status > "$record/dkms-after.txt" 2>&1 + find /boot /usr/lib/modules -maxdepth 2 -type f \ + \( -name 'vmlinuz*' -o -name 'initramfs*' -o -name 'initrd*' -o -name pkgbase \) \ + -printf '%p size=%s mtime=%TY-%Tm-%TdT%TH:%TM:%TS\n' | sort > "$record/kernel-boot-artifacts-after.txt" 2>&1 + docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' | sort > "$record/containers-after.txt" 2>&1 + find "$record" -maxdepth 1 -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum \ + > "$record/SHA256SUMS" 2>&1 + chmod -R go-rwx "$record" + + printf 'PACKAGE TRANSACTION COMPLETED; REBOOT NOT PERFORMED.\n' + printf 'MAINTENANCE_RECORD=%s\n' "$record" + printf 'Review all warnings, hooks, services, kernel artifacts, and pacnew/pacsave files.\n' + printf 'Reboot and rootless-Docker provisioning remain separate approval gates.\n' + return 0 +} + +main "$@" diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh new file mode 100644 index 0000000..cd089e5 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# Read-only validation plus root-only evidence recording. Run as a separate process. + +add_failure() { + printf '%s\n' "$1" >> "$post_record/failed-checks.txt" + failures=$((failures + 1)) + return 0 +} + +check() { + label=$1; outfile=$2; shift 2 + "$@" > "$post_record/$outfile" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "$label|rc=$rc|file=$outfile"; fi + return 0 +} + +check_shell() { + label=$1; outfile=$2; command_text=$3 + /usr/bin/bash -o pipefail -c "$command_text" > "$post_record/$outfile" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "$label|rc=$rc|file=$outfile"; fi + return 0 +} + +validate_health_url_file() { + file=$1 + if [ ! -s "$file" ]; then add_failure 'health-urls|missing'; return 0; fi + while IFS= read -r url; do + case "$url" in https://*) ;; *) add_failure 'health-url|unsupported-scheme'; continue ;; esac + authority=${url#https://}; authority=${authority%%/*} + case "$url" in *'?'*|*'#'*|*$'\n'*|*$'\r'*|*$'\t'*|*' '*) add_failure 'health-url|unsafe-content'; continue ;; esac + case "$authority" in ''|*@*) add_failure 'health-url|userinfo-or-empty-authority'; continue ;; esac + done < "$file" + return 0 +} + +compare_containers() { + : > "$post_record/container-comparison.psv" + while IFS= read -r container; do + [ -n "$container" ] || continue + running=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null) + printf '%s|running=%s|health=%s\n' "$container" "${running:-missing}" "${health:-unknown}" \ + >> "$post_record/container-comparison.psv" + if [ "$running" != true ] || [ "$health" = unhealthy ]; then + add_failure "container-regression|name=$container|running=${running:-missing}|health=${health:-unknown}" + fi + done < "$baseline/running-containers-before.txt" + return 0 +} + +compare_units() { + : > "$post_record/unit-comparison.psv" + while IFS='|' read -r unit old_load old_active old_sub; do + [ -n "$unit" ] || continue + new_load=$(systemctl show "$unit" -p LoadState --value 2>/dev/null) + new_active=$(systemctl show "$unit" -p ActiveState --value 2>/dev/null) + new_sub=$(systemctl show "$unit" -p SubState --value 2>/dev/null) + printf '%s|before=%s,%s,%s|after=%s,%s,%s\n' \ + "$unit" "$old_load" "$old_active" "$old_sub" "${new_load:-missing}" "${new_active:-missing}" "${new_sub:-missing}" \ + >> "$post_record/unit-comparison.psv" + if [ "$old_load" = loaded ] && [ "$new_load" != loaded ]; then + add_failure "critical-unit-missing|unit=$unit" + elif [ "$old_active" = active ] && [ "$new_active" != active ]; then + add_failure "critical-unit-not-active|unit=$unit|state=${new_active:-missing}" + fi + done < "$baseline/critical-units-baseline.psv" + return 0 +} + +validate_databases() { + : > "$post_record/database-validation.txt" + while IFS='|' read -r model engine identity state detail; do + [ -n "$model" ] || continue + if [ "$model" = unit ]; then + active=$(systemctl show "$identity" -p ActiveState --value 2>/dev/null) + printf '%s|%s|unit=%s|active=%s\n' "$model" "$engine" "$identity" "${active:-missing}" \ + >> "$post_record/database-validation.txt" + if [ "$active" != active ]; then add_failure "database-unit-not-active|unit=$identity"; fi + if [ "$engine" = mysql ]; then + mysqladmin ping >> "$post_record/database-validation.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || add_failure "mysqladmin-ping-failed|unit=$identity|rc=$rc" + elif [ "$engine" = postgresql ]; then + pg_isready >> "$post_record/database-validation.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || add_failure "pg-isready-failed|unit=$identity|rc=$rc" + fi + elif [ "$model" = container ]; then + running=$(docker inspect -f '{{.State.Running}}' "$identity" 2>/dev/null) + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$identity" 2>/dev/null) + printf '%s|%s|container=%s|running=%s|health=%s\n' \ + "$model" "$engine" "$identity" "${running:-missing}" "${health:-unknown}" \ + >> "$post_record/database-validation.txt" + if [ "$running" != true ] || [ "$health" = unhealthy ]; then + add_failure "database-container-failed|container=$identity|running=${running:-missing}|health=${health:-unknown}" + fi + fi + done < "$baseline/database-baseline.psv" + return 0 +} + +check_package_files() { + pacman -Qkk > "$post_record/pacman-Qkk.txt" 2>&1 + qkk_rc=$? + printf 'pacman-Qkk-return=%s\n' "$qkk_rc" > "$post_record/pacman-Qkk-classification.txt" + grep -E -i 'missing|could not read|permission denied|unreadable' "$post_record/pacman-Qkk.txt" \ + > "$post_record/pacman-Qkk-missing-or-unreadable.txt" 2>/dev/null + missing_rc=$? + grep -E -i 'altered|modified' "$post_record/pacman-Qkk.txt" \ + > "$post_record/pacman-Qkk-modified.txt" 2>/dev/null + if [ "$missing_rc" -eq 0 ] && [ -s "$post_record/pacman-Qkk-missing-or-unreadable.txt" ]; then + add_failure 'packaged-files-missing-or-unreadable' + fi + printf 'Modified configuration/package files require manual classification; no repair performed.\n' \ + >> "$post_record/pacman-Qkk-classification.txt" + return 0 +} + +main() { + failures=0 + maintenance=${1:-} + if [ "$(id -u)" -ne 0 ]; then + printf 'HARD_STOP: run as a separate root process\n' >&2 + return 10 + fi + if [ -z "$maintenance" ] || [ ! -d "$maintenance" ] || [ -L "$maintenance" ]; then + printf 'HARD_STOP: supply the exact maintenance record path\n' >&2 + return 11 + fi + owner=$(stat -c %u "$maintenance" 2>/dev/null); mode=$(stat -c %a "$maintenance" 2>/dev/null) + if [ "$owner" != 0 ] || [ "$mode" != 700 ]; then + printf 'HARD_STOP: maintenance record must be root-owned mode 0700\n' >&2 + return 12 + fi + baseline=$maintenance/preflight-record + required='running-containers-before.txt critical-units-baseline.psv database-baseline.psv health-urls.sanitized inventory-hashes.sha256 result.manifest result.manifest.sha256' + for file in $required; do + if [ ! -f "$baseline/$file" ] || [ -L "$baseline/$file" ]; then + printf 'HARD_STOP: missing baseline file %s\n' "$file" >&2 + return 13 + fi + done + (cd "$baseline" && sha256sum -c inventory-hashes.sha256 > /dev/null 2>&1) + rc=$? + if [ "$rc" -ne 0 ]; then + printf 'HARD_STOP: copied preflight baseline checksum failure\n' >&2 + return 14 + fi + (cd "$baseline" && sha256sum -c result.manifest.sha256 > /dev/null 2>&1) + rc=$? + if [ "$rc" -ne 0 ]; then + printf 'HARD_STOP: copied preflight result manifest checksum failure\n' >&2 + return 14 + fi + + umask 077 + install -d -o root -g root -m 0700 /var/log/le-phase2a-post-reboot + rc=$?; [ "$rc" -eq 0 ] || { printf 'HARD_STOP: cannot create post-reboot record parent\n' >&2; return 15; } + post_record=$(mktemp -d "/var/log/le-phase2a-post-reboot/validation.$(date -u +%Y%m%dT%H%M%SZ).XXXXXXXX") + rc=$? + if [ "$rc" -ne 0 ] || [ -z "$post_record" ] || [ ! -d "$post_record" ]; then + printf 'HARD_STOP: cannot atomically create unique post-reboot record\n' >&2 + return 16 + fi + chown root:root "$post_record"; chmod 0700 "$post_record" + : > "$post_record/failed-checks.txt" + printf 'maintenance_record=%s\n' "$maintenance" > "$post_record/source.txt" + + check 'running-kernel' running-kernel.txt uname -a + check_shell 'installed-kernels' installed-kernels.txt 'pacman -Q linux linux-headers linux-zen linux-zen-headers' + check 'failed-units' failed-units.txt systemctl --failed --no-legend --plain --no-pager + if [ -s "$post_record/failed-units.txt" ]; then add_failure 'failed-units-present'; fi + check_shell 'ssh-state' ssh-state.txt \ + 'systemctl show sshd.service -p LoadState -p ActiveState -p SubState -p ExecMainStatus; journalctl -b -u sshd.service --no-pager' + check_shell 'docker-daemon' docker-daemon.txt \ + "systemctl show docker.service -p LoadState -p ActiveState -p SubState -p ExecMainStatus; docker info" + check_shell 'all-containers' containers-after.psv \ + "docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' | sort" + compare_containers + compare_units + validate_databases + + check_shell 'mounts-and-capacity' mounts-capacity.txt 'findmnt --real; df -hT / /boot /var/cache/pacman/pkg /tmp' + check_shell 'network-routes-firewall-ports' network-after.txt \ + 'ip -brief address; ip route show table all; ip -6 route show table all; nft list ruleset; ss -lntup' + wg show > "$post_record/wireguard-after.txt" 2>&1 + wg_rc=$?; if [ "$wg_rc" -ne 0 ]; then printf 'wireguard-observation|rc=%s\n' "$wg_rc" > "$post_record/warnings.txt"; fi + + validate_health_url_file "$baseline/health-urls.sanitized" + : > "$post_record/public-health.txt" + while IFS= read -r url; do + metrics=$(curl --fail --silent --show-error --max-redirs 0 \ + --connect-timeout 10 --max-time 30 --output /dev/null \ + --write-out 'http=%{http_code}|remote=%{remote_ip}|time=%{time_total}' \ + "$url" 2>> "$post_record/public-health-errors.txt") + rc=$? + printf '%s|%s\n' "$url" "$metrics" >> "$post_record/public-health.txt" + if [ "$rc" -ne 0 ]; then add_failure "public-health-failed|url=$url|rc=$rc"; fi + done < "$baseline/health-urls.sanitized" + + pacman -Dk > "$post_record/pacman-Dk.txt" 2>&1 + dk_rc=$? + if [ "$dk_rc" -ne 0 ]; then add_failure "pacman-Dk-failed|rc=$dk_rc"; fi + check_package_files + check_shell 'target-packages' target-packages.txt 'pacman -Q rootlesskit slirp4netns openai-codex' + check_shell 'pacnew-pacsave-inventory' pacnew-pacsave.txt \ + "find /etc -xdev -type f \( -name '*.pacnew' -o -name '*.pacsave' \) -printf '%p\\n' | sort; pacdiff -o 2>&1" + + find "$post_record" -maxdepth 1 -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum \ + > "$post_record/SHA256SUMS" 2>&1 + chmod -R go-rwx "$post_record" + printf 'POST_REBOOT_RECORD=%s\n' "$post_record" + if [ "$failures" -ne 0 ]; then + printf 'POST_REBOOT_RESULT=HARD_STOP failures=%s\n' "$failures" + return 20 + fi + printf 'POST_REBOOT_RESULT=PASS failures=0\n' + printf 'Rootless-Docker provisioning remains separately approval-gated.\n' + return 0 +} + +main "$@" diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh new file mode 100644 index 0000000..b6f79b0 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +# Read-only host inspection plus root-only evidence recording. +# Run as a separate process; this script changes no shell options and uses return only. + +add_failure() { + printf '%s\n' "$1" >> "$record/failed-mandatory-checks.txt" + overall=HARD_STOP + return 0 +} + +mandatory() { + label=$1 + outfile=$2 + shift 2 + "$@" > "$record/$outfile" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + add_failure "$label|rc=$rc|file=$outfile" + fi + return 0 +} + +mandatory_shell() { + label=$1 + outfile=$2 + command_text=$3 + /usr/bin/bash -o pipefail -c "$command_text" > "$record/$outfile" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + add_failure "$label|rc=$rc|file=$outfile" + fi + return 0 +} + +optional_shell() { + label=$1 + outfile=$2 + command_text=$3 + /usr/bin/bash -o pipefail -c "$command_text" > "$record/$outfile" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + printf '%s|rc=%s|file=%s\n' "$label" "$rc" "$outfile" >> "$record/warnings.txt" + fi + return 0 +} + +validate_health_urls() { + : > "$record/health-urls.sanitized" + if [ -z "${PUBLIC_HEALTH_URLS:-}" ]; then + add_failure 'public-health-urls|unset' + return 0 + fi + case "$PUBLIC_HEALTH_URLS" in + *$'\n'*|*$'\r'*|*$'\t'*) + add_failure 'public-health-urls|control-character-rejected' + return 0 + ;; + esac + for url in $PUBLIC_HEALTH_URLS; do + case "$url" in + https://*) ;; + *) add_failure 'public-health-url|unsupported-scheme-or-malformed'; continue ;; + esac + authority=${url#https://} + authority=${authority%%/*} + case "$url" in + *'?'*|*'#'*|*$'\n'*|*$'\r'*|*$'\t'*|*' '*) + add_failure 'public-health-url|query-fragment-or-control-character-rejected' + continue + ;; + esac + case "$authority" in + ''|*@*) add_failure 'public-health-url|empty-authority-or-userinfo-rejected'; continue ;; + esac + printf '%s\n' "$url" >> "$record/health-urls.sanitized" + done + if [ ! -s "$record/health-urls.sanitized" ]; then + add_failure 'public-health-urls|no-valid-non-secret-https-url' + fi + return 0 +} + +check_public_health() { + : > "$record/public-health.txt" + while IFS= read -r url; do + [ -n "$url" ] || continue + metrics=$(curl --fail --silent --show-error --max-redirs 0 \ + --connect-timeout 10 --max-time 30 --output /dev/null \ + --write-out 'http=%{http_code}|remote=%{remote_ip}|time=%{time_total}' \ + "$url" 2>> "$record/public-health-errors.txt") + rc=$? + printf '%s|%s\n' "$url" "$metrics" >> "$record/public-health.txt" + if [ "$rc" -ne 0 ]; then + add_failure "public-health|rc=$rc|url=$url" + fi + done < "$record/health-urls.sanitized" + return 0 +} + +capture_bootloader() { + found=0 + : > "$record/bootloader-inventory.txt" + if [ -d /boot/loader/entries ]; then + printf '%s\n' 'type=systemd-boot' >> "$record/bootloader-inventory.txt" + bootctl status >> "$record/bootloader-inventory.txt" 2>&1 + bootctl_rc=$? + find /boot/loader/entries -maxdepth 1 -type f -print -exec sed -n '1,160p' {} \; \ + >> "$record/bootloader-inventory.txt" 2>&1 + find_rc=$? + if [ "$bootctl_rc" -eq 0 ] && [ "$find_rc" -eq 0 ]; then found=1; fi + fi + if [ -f /boot/grub/grub.cfg ]; then + printf '%s\n' 'type=grub' >> "$record/bootloader-inventory.txt" + grub-install --version >> "$record/bootloader-inventory.txt" 2>&1 + grub_rc=$? + grep -E '^menuentry |^submenu |linux.*vmlinuz|initrd' /boot/grub/grub.cfg \ + >> "$record/bootloader-inventory.txt" 2>&1 + grep_rc=$? + if [ "$grub_rc" -eq 0 ] && [ "$grep_rc" -eq 0 ]; then found=1; fi + fi + if [ "$found" -ne 1 ]; then + add_failure 'bootloader-inventory|no-verified-supported-bootloader' + fi + return 0 +} + +capture_unit_baseline() { + systemctl list-units --type=service --all --no-legend --no-pager \ + > "$record/all-service-units.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + add_failure "service-unit-inventory|rc=$rc" + return 0 + fi + : > "$record/critical-units-baseline.psv" + awk '{print $1}' "$record/all-service-units.txt" | while IFS= read -r unit; do + case "$unit" in + docker.service|nginx.service|php-fpm.service|mariadb.service|mysqld.service|postgresql.service|postfix.service|dovecot.service|rspamd.service|forgejo.service|sshd.service) + ;; + *) continue ;; + esac + load=$(systemctl show "$unit" -p LoadState --value 2>/dev/null) + active=$(systemctl show "$unit" -p ActiveState --value 2>/dev/null) + sub=$(systemctl show "$unit" -p SubState --value 2>/dev/null) + printf '%s|%s|%s|%s\n' "$unit" "$load" "$active" "$sub" + done > "$record/critical-units-baseline.psv" + for unit in ${EXTRA_CRITICAL_UNITS:-}; do + load=$(systemctl show "$unit" -p LoadState --value 2>/dev/null) + if [ "$load" = loaded ]; then + active=$(systemctl show "$unit" -p ActiveState --value 2>/dev/null) + sub=$(systemctl show "$unit" -p SubState --value 2>/dev/null) + printf '%s|%s|%s|%s\n' "$unit" "$load" "$active" "$sub" \ + >> "$record/critical-units-baseline.psv" + else + add_failure "extra-critical-unit|not-loaded|unit=$unit" + fi + done + sort -u -o "$record/critical-units-baseline.psv" "$record/critical-units-baseline.psv" + return 0 +} + +capture_database_baseline() { + : > "$record/database-baseline.psv" + while IFS='|' read -r unit load active sub; do + [ "$active" = active ] || continue + case "$unit" in + mariadb.service|mysqld.service) printf 'unit|mysql|%s|%s|%s\n' "$unit" "$active" "$sub" ;; + postgresql.service) printf 'unit|postgresql|%s|%s|%s\n' "$unit" "$active" "$sub" ;; + esac + done < "$record/critical-units-baseline.psv" >> "$record/database-baseline.psv" + while IFS='|' read -r name image status ports; do + case "$status" in Up\ *) ;; *) continue ;; esac + lowered=$(printf '%s %s' "$name" "$image" | tr '[:upper:]' '[:lower:]') + case "$lowered" in + *maria*|*mysql*) printf 'container|mysql|%s|%s|%s\n' "$name" "$status" "$ports" ;; + *postgres*) printf 'container|postgresql|%s|%s|%s\n' "$name" "$status" "$ports" ;; + esac + done < "$record/containers-all.psv" >> "$record/database-baseline.psv" + return 0 +} + +validate_database_baseline() { + : > "$record/database-readiness-before.txt" + while IFS='|' read -r model engine identity state detail; do + [ "$model" = unit ] || continue + if [ "$engine" = mysql ]; then + mysqladmin ping >> "$record/database-readiness-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || add_failure "mysqladmin-ping|unit=$identity|rc=$rc" + elif [ "$engine" = postgresql ]; then + pg_isready >> "$record/database-readiness-before.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || add_failure "pg-isready|unit=$identity|rc=$rc" + fi + done < "$record/database-baseline.psv" + while IFS='|' read -r model engine identity state detail; do + [ "$model" = container ] || continue + running=$(docker inspect -f '{{.State.Running}}' "$identity" 2>/dev/null) + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$identity" 2>/dev/null) + printf '%s|%s|running=%s|health=%s\n' "$engine" "$identity" "${running:-missing}" "${health:-unknown}" \ + >> "$record/database-readiness-before.txt" + if [ "$running" != true ] || [ "$health" = unhealthy ]; then + add_failure "database-container|name=$identity|running=${running:-missing}|health=${health:-unknown}" + fi + done < "$record/database-baseline.psv" + return 0 +} + +main() { + if [ "$(id -u)" -ne 0 ]; then + printf 'HARD_STOP: run as a separate root process so records are root-only\n' >&2 + return 10 + fi + umask 077 + install -d -o root -g root -m 0700 /var/log/le-phase2a-preflight + rc=$? + if [ "$rc" -ne 0 ]; then + printf 'HARD_STOP: cannot create root-only preflight record parent\n' >&2 + return 11 + fi + record=$(mktemp -d "/var/log/le-phase2a-preflight/preflight.$(date -u +%Y%m%dT%H%M%SZ).XXXXXXXX") + rc=$? + if [ "$rc" -ne 0 ] || [ -z "$record" ] || [ ! -d "$record" ]; then + printf 'HARD_STOP: cannot atomically create unique preflight record\n' >&2 + return 12 + fi + chown root:root "$record" + chmod 0700 "$record" + + overall=PASS + : > "$record/failed-mandatory-checks.txt" + : > "$record/warnings.txt" + timestamp_epoch=$(date +%s) + timestamp_utc=$(date -u +%FT%TZ) + hostname_value=$(hostname) + boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) + running_kernel=$(uname -r) + [ -n "$hostname_value" ] || add_failure 'hostname|empty' + [ -n "$boot_id" ] || add_failure 'boot-id|unavailable' + [ -n "$running_kernel" ] || add_failure 'running-kernel|unavailable' + [ "${LE_PHASE2A_RECOVERY_READY:-}" = YES ] || add_failure 'recovery-access-attestation|missing' + [ "${LE_PHASE2A_BACKUPS_READY:-}" = YES ] || add_failure 'backup-readiness-attestation|missing' + + mandatory 'uptime' uptime.txt uptime + mandatory 'logged-in-sessions' logged-in-sessions.txt who -a + mandatory 'filesystem-capacity' filesystem-capacity.txt df -hT / /boot /var/cache/pacman/pkg /tmp + mandatory 'filesystem-inodes' filesystem-inodes.txt df -hi / /boot /var/cache/pacman/pkg /tmp + mandatory 'installed-package-inventory' packages-before.txt pacman -Q + mandatory 'failed-unit-inventory' failed-units-before.txt systemctl --failed --no-legend --plain --no-pager + if [ -s "$record/failed-units-before.txt" ]; then add_failure 'failed-units|present'; fi + mandatory_shell 'kernel-initramfs-inventory' kernel-initramfs-before.txt \ + "find /usr/lib/modules /boot -maxdepth 2 -type f \\ + \( -name vmlinuz -o -name 'vmlinuz-*' -o -name 'initramfs*' -o -name 'initrd*' -o -name pkgbase \) \\ + -printf '%p size=%s mtime=%TY-%Tm-%TdT%TH:%TM:%TS\\n' | sort; test \"\$(find /boot -maxdepth 1 -type f \\ + \( -name 'vmlinuz-*' -o -name 'initramfs*' -o -name 'initrd*' \) | wc -l)\" -gt 0" + capture_bootloader + optional_shell 'dkms-status' dkms-before.txt 'command -v dkms >/dev/null && dkms status' + mandatory_shell 'docker-container-inventory' containers-all.psv \ + "docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' | sort" + mandatory_shell 'running-container-inventory' running-containers-before.txt \ + "docker ps --format '{{.Names}}' | sort" + capture_unit_baseline + capture_database_baseline + validate_database_baseline + + validate_health_urls + check_public_health + mandatory_shell 'network-firewall-listener-inventory' network-firewall-before.txt \ + 'findmnt --real; ip -brief address; ip route show table all; ip -6 route show table all; nft list ruleset; ss -lntup' + optional_shell 'wireguard-observation' wireguard-before.txt 'wg show' + mandatory 'package-database-consistency' pacman-Dk-before.txt pacman -Dk + optional_shell 'packaged-file-consistency-observation' pacman-Qkk-before.txt 'pacman -Qkk' + grep -E -i 'missing|could not read|permission denied|unreadable' "$record/pacman-Qkk-before.txt" \ + > "$record/pacman-Qkk-missing-or-unreadable-before.txt" 2>/dev/null + if [ -s "$record/pacman-Qkk-missing-or-unreadable-before.txt" ]; then + add_failure 'packaged-files-missing-or-unreadable-before-maintenance' + fi + mandatory_shell 'pacnew-pacsave-inventory' pacnew-pacsave-before.txt \ + "find /etc -xdev -type f \( -name '*.pacnew' -o -name '*.pacsave' \) -printf '%p\\n' | sort" + mandatory_shell 'migration-git-state' migration-git-state.txt \ + 'git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration status --short --branch; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse HEAD; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse origin/migration/le-app-database' + mandatory_shell 'preview-report-integrity' preview-report-checksum.txt \ + "printf '%s %s\\n' 195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d /tmp/le-phase2-package-decision-20260712.Mn7mIx/phase2-package-decision-report.md | sha256sum -c -" + + inventory_files='packages-before.txt failed-units-before.txt kernel-initramfs-before.txt bootloader-inventory.txt containers-all.psv running-containers-before.txt critical-units-baseline.psv database-baseline.psv database-readiness-before.txt health-urls.sanitized public-health.txt network-firewall-before.txt pacman-Dk-before.txt pacman-Qkk-before.txt pacman-Qkk-missing-or-unreadable-before.txt pacnew-pacsave-before.txt migration-git-state.txt preview-report-checksum.txt' + ( + cd "$record" || return 1 + sha256sum $inventory_files > inventory-hashes.sha256 + sha256sum -c inventory-hashes.sha256 > inventory-hash-verification.txt + ) + hash_rc=$? + if [ "$hash_rc" -ne 0 ]; then + add_failure "inventory-hashes|rc=$hash_rc" + fi + hashes_hash=$(sha256sum "$record/inventory-hashes.sha256" | awk '{print $1}') + + failure_count=$(wc -l < "$record/failed-mandatory-checks.txt") + failed_checks_hash=$(sha256sum "$record/failed-mandatory-checks.txt" | awk '{print $1}') + { + printf 'format=le-phase2a-preflight-v1\n' + printf 'hostname=%s\n' "$hostname_value" + printf 'boot_id=%s\n' "$boot_id" + printf 'running_kernel=%s\n' "$running_kernel" + printf 'timestamp_epoch=%s\n' "$timestamp_epoch" + printf 'timestamp_utc=%s\n' "$timestamp_utc" + printf 'overall=%s\n' "$overall" + printf 'failure_count=%s\n' "$failure_count" + printf 'failed_checks_file=failed-mandatory-checks.txt\n' + printf 'failed_checks_sha256=%s\n' "$failed_checks_hash" + printf 'inventory_hashes_sha256=%s\n' "$hashes_hash" + } > "$record/result.manifest" + (cd "$record" && sha256sum result.manifest > result.manifest.sha256) + rc=$? + if [ "$rc" -ne 0 ]; then + overall=HARD_STOP + printf '%s\n' "result-manifest-checksum|rc=$rc" >> "$record/failed-mandatory-checks.txt" + fi + chmod -R go-rwx "$record" + printf 'PREFLIGHT_RECORD=%s\n' "$record" + printf 'PREFLIGHT_RESULT=%s failures=%s\n' "$overall" "$failure_count" + if [ "$overall" != PASS ]; then + return 20 + fi + return 0 +} + +main "$@" diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/reboot-procedure.md b/docs/le-app-database-migration/phase2a-host-maintenance/reboot-procedure.md new file mode 100644 index 0000000..3d0fe9c --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/reboot-procedure.md @@ -0,0 +1,65 @@ +# Separate reboot procedure + +This procedure begins only after successful package execution and review of its unique maintenance record. It never reboots automatically. Maintenance and validation records are atomic unique root-owned mode `0700` directories. They can contain sensitive operational metadata and must not be committed. + +## Pre-reboot verification + +Set the exact record path printed by `execute-maintenance.sh`: + +```bash +record=/var/log/le-phase2a-maintenance/maintenance.YYYYMMDDTHHMMSSZ.XXXXXXXX +test -d "$record" || { printf 'STOP: invalid record path\n' >&2; return 1; } +``` + +Record the running kernel and review installed kernel packages: + +```bash +uname -a | tee "$record/running-kernel-immediately-before-reboot.txt" +pacman -Q linux linux-headers linux-zen linux-zen-headers | + tee "$record/installed-kernels-immediately-before-reboot.txt" +``` + +Verify both new module trees, kernel images, and nonempty initramfs files: + +```bash +find /usr/lib/modules /boot -maxdepth 2 -type f \ + \( -name vmlinuz -o -name 'vmlinuz-*' -o -name 'initramfs*' -o -name 'initrd*' -o -name pkgbase \) \ + -printf '%p size=%s mtime=%TY-%Tm-%TdT%TH:%TM:%TS\n' | + sort | tee "$record/verified-kernel-initramfs-files.txt" + +find /boot -maxdepth 1 -type f \ + \( -name 'vmlinuz-*' -o -name 'initramfs*' -o -name 'initrd*' \) -size 0 -print +``` + +Any zero-length artifact is a hard stop. Review DKMS and package hook output: + +```bash +dkms status | tee "$record/dkms-immediately-before-reboot.txt" +systemctl --failed --no-pager | tee "$record/failed-units-immediately-before-reboot.txt" +sed -n '1,240p' "$record/hook-restart-summary.txt" +``` + +Confirm bootloader state without changing it: + +```bash +bootctl status 2>&1 | tee "$record/bootctl-status-before-reboot.txt" +find /boot/loader/entries -maxdepth 1 -type f -print -exec sed -n '1,160p' {} \; 2>/dev/null | + tee "$record/systemd-boot-entries-before-reboot.txt" + +grub-install --version 2>&1 | tee "$record/grub-version-before-reboot.txt" +test -f /boot/grub/grub.cfg && + grep -E "^menuentry |^submenu |linux.*vmlinuz|initrd" /boot/grub/grub.cfg | + tee "$record/grub-entries-before-reboot.txt" +``` + +The operator must identify and record both the intended new-kernel entry and a known-good previous-kernel/fallback entry. Stop if boot entries or initramfs references are missing, DKMS failed, storage is degraded, backups are unavailable, SSH is unstable, or production is unhealthy. + +## Separately approved reboot command + +Only after explicit approval for this exact reboot: + +```bash +sudo systemctl reboot +``` + +Do not embed this command in the maintenance script, a timer, `at`, a shell trap, or a command chain. Keep independent console access open and proceed to `post-reboot-validate.sh` after the host returns. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/rollback-recovery.md b/docs/le-app-database-migration/phase2a-host-maintenance/rollback-recovery.md new file mode 100644 index 0000000..a802118 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/rollback-recovery.md @@ -0,0 +1,59 @@ +# Rollback and recovery + +Rollback is emergency recovery, not a routine alternative to a complete Arch upgrade. Stop and coordinate through the operator whenever possible. + +## New kernel fails + +1. Use the independent console or bootloader menu. +2. Select the previously verified kernel or fallback initramfs entry recorded before reboot. +3. Do not repeatedly boot the failed entry or regenerate boot artifacts blindly. +4. After the previous kernel boots, record `uname -a`, boot journal, failed units, DKMS state, `/boot` contents, and bootloader entries. +5. Keep production stopped or degraded only as required for safety; obtain explicit approval before rebuilding initramfs, changing boot defaults, or removing a kernel. + +## Package recovery + +The first recovery source is an exact, validated package archive still present in `/var/cache/pacman/pkg`. Arch Linux Archive is the fallback source when the exact archive is unavailable. + +Emergency package restoration must: + +- use the exact before-inventory and transaction log; +- restore a coherent dependency set, not one guessed library; +- verify package signatures and checksums; +- preserve full command output; +- be approved as emergency recovery; +- be followed by `pacman -Dk`, package/file consistency checks, initramfs/DKMS/bootloader validation, and production health tests. + +Do not improvise a broad downgrade, use `--overwrite` casually, disable signature checking, delete the package database lock without proving no Pacman process exists, or mix repository dates. Arch does not support partial upgrades; a downgrade can require an internally consistent dated repository snapshot. + +## SSH recovery + +- Expect the OpenSSH hook to restart a running `sshd.service` during the transaction. +- Keep the active session open and confirm a second session before reboot when the maintenance plan allows. +- If SSH fails, use the independent console. Inspect `sshd -t`, service status/journal, host keys, PAM, `sshd_config`, and any new `.pacnew` without replacing configuration automatically. +- Do not weaken authentication, expose a new port, copy keys, or disable the firewall as a shortcut. + +## Docker and service recovery + +- Compare against `running-containers-before.txt` and `containers-before.txt`. +- Inspect Docker daemon and container logs, mounts, networks, health checks, and storage availability before restarting anything. +- Restart only an identified failed service with operator approval; do not restart all services or recreate containers/volumes indiscriminately. +- Never delete Docker data, volumes, databases, images, or production configuration as recovery. +- Database recovery follows the service-specific backup/runbook and requires explicit approval before any mutation. + +## Mandatory stop conditions + +Stop rather than improvise on: + +- package signature/keyring/database errors; +- interrupted Pacman transaction or uncertain package state; +- failed kernel, initramfs, DKMS, or bootloader generation; +- missing previous-kernel boot option or loss of console access; +- SSH/PAM/network/firewall configuration ambiguity; +- new unexplained `.pacnew`/`.pacsave` for critical configuration; +- storage, filesystem, database, Docker-volume, or backup inconsistency; +- failed production database, proxy, mail, Forgejo, Docker, or public endpoint; +- any proposed action that would modify production data/configuration, systemd, nftables, credentials, or package history beyond the approved recovery scope. + +Rootless-Docker provisioning and contained Codex setup are never part of recovery. Resume them only after the host is healthy and a later phase is explicitly approved. + +All recovery evidence remains in the root-only unique preflight, maintenance, and post-reboot record directories. It may contain operationally sensitive metadata and must not be committed or copied into a user-readable workspace. From da03be61a9ce984773d8420c4d737aa959902278 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 17:36:55 -0400 Subject: [PATCH 08/14] refresh Phase 2A transaction and readiness gates --- .../host-runtime-plan.md | 4 +- .../implementation-plan.md | 2 + .../phase2a-host-maintenance/MANIFEST.sha256 | 24 +- .../phase2a-host-maintenance/README.md | 46 +- .../approved-transaction/README.md | 20 + .../approved-transaction/SHA256SUMS | 12 + .../approved-transaction/backups.txt | 147 +++ .../comparison-to-221-preview.diff | 10 + .../host-hooks-inventory.txt | 954 ++++++++++++++++++ .../installed-versions.txt | 16 + .../approved-transaction/inventory.psv | 220 ++++ .../approved-transaction/new-packages.psv | 5 + .../normalized-actions.psv | 220 ++++ .../approved-transaction/packaged-hooks.txt | 3 + .../approved-transaction/scriptlets.txt | 8 + .../approved-transaction/totals.psv | 8 + .../approved-transaction/upgrades.psv | 215 ++++ .../execute-maintenance.sh | 163 ++- .../failed-unit-disposition/README.md | 15 + .../failed-unit-disposition/SHA256SUMS | 2 + .../drkonqi-failed-units.allowlist.psv | 234 +++++ .../post-reboot-validate.sh | 39 +- .../preflight-read-only.sh | 79 +- 23 files changed, 2409 insertions(+), 37 deletions(-) create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/README.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/SHA256SUMS create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/backups.txt create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/comparison-to-221-preview.diff create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/host-hooks-inventory.txt create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/installed-versions.txt create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/inventory.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/new-packages.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/normalized-actions.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/packaged-hooks.txt create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/scriptlets.txt create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/totals.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/upgrades.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/README.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/SHA256SUMS create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/drkonqi-failed-units.allowlist.psv diff --git a/docs/le-app-database-migration/host-runtime-plan.md b/docs/le-app-database-migration/host-runtime-plan.md index b33516f..a8faeac 100644 --- a/docs/le-app-database-migration/host-runtime-plan.md +++ b/docs/le-app-database-migration/host-runtime-plan.md @@ -50,7 +50,7 @@ The last isolated preview of: pacman -Syu --needed rootlesskit slirp4netns openai-codex ``` -was a 221-package full host upgrade affecting both installed kernels and OpenSSH. It is a host-maintenance operation requiring a fresh exact transaction preview, reboot planning, service-health checks, `.pacnew` review, and separate operator approval. A partial Arch upgrade is not acceptable. +was refreshed after removal of unused NoMachine and is now a 220-package full host upgrade (215 upgrades and 5 new packages) affecting both installed kernels and OpenSSH. It requires reboot planning, service-health checks, `.pacnew` review, explicit Snapper snapshot verification, and separate operator approval. The checksummed baseline is in `phase2a-host-maintenance/approved-transaction/`. A partial Arch upgrade is not acceptable. The package paths are: @@ -150,7 +150,7 @@ TasksMax=4096 These limits cover Codex, RootlessKit, `dockerd`, containerd, builds, jobs, and all containers. Validate generated properties and cgroup delegation before relying on the limits. -Disk-growth containment remains unresolved. Btrfs quotas were disabled at the last observation. Do not enable quotas, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit as an incidental provisioning step. Filesystem visibility constrains where the account may write but does not cap growth within `/srv/le-app-codex`. The operator must separately choose and approve a disk-quota policy. +Disk-growth containment remains unresolved. Btrfs quotas remain intentionally disabled. The repaired `/.snapshots` Btrfs subvolume and successful Snapper snapshots 1, 2, and 3 establish snapshot operation, not quota accounting. Do not enable quotas, run `snapper setup-quota`, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit incidentally. Filesystem visibility constrains where the account may write but does not cap growth within `/srv/le-app-codex`. The operator must separately choose and approve a disk-quota policy. Soft planning budgets remain useful for monitoring: 80 GiB container runtime, 25 GiB PostgreSQL, 15 GiB media, 10 GiB legacy uploads, 35 GiB BGBye models, 10 GiB temporary data, 5 GiB browser artifacts, 10 GiB migration reports, and 10 GiB checkout/home metadata. Startup should fail when host/project free-space thresholds are unsafe, but soft checks do not replace a hard quota. diff --git a/docs/le-app-database-migration/implementation-plan.md b/docs/le-app-database-migration/implementation-plan.md index 93e3583..194700e 100644 --- a/docs/le-app-database-migration/implementation-plan.md +++ b/docs/le-app-database-migration/implementation-plan.md @@ -18,6 +18,8 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt - Use deterministic CPU BGBye initially; defer optional rootless GPU provisioning. - Approve browser-profile-scoped local CA trust for HTTPS testing. - Resolve authentication and disk-quota policy separately; confirm nonproduction storage/capacity locations. + - Use the refreshed post-NoMachine 220-package checksummed baseline; require exact disposition of the reviewed 234 DrKonqi desktop crash-processing failures and hard-stop on any fingerprint change or other failed unit. + - Verify the repaired `/.snapshots` Btrfs subvolume and create, number, and independently verify an explicit pre-upgrade Snapper snapshot; do not enable Btrfs quotas or treat unavailable quota accounting as snapshot failure. - Execute every inert positive and negative preflight in `host-runtime-plan.md` before starting the user manager, Docker, Codex, or application work. 3. **Reversible canonical-path consolidation — separately approval-gated** diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 index 486cbbd..d583d39 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 +++ b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 @@ -1,6 +1,22 @@ -756dfad7eddf89406383804f891266bd814c6ca974a5a250fddc72b67c055ddb README.md -9d9db1819ef473954ea9f5009dc985f3b8b090406246bb22a10ffa09caf3e692 preflight-read-only.sh -ab7220b2042b6a256c26189b37e66637cc7cf741420265ecefd94ab6fa6c06b7 execute-maintenance.sh +9934efd3946efad746823ebbeb46875cc0ae1e14b5b2b3e399a384c3654bcd2a README.md +28ebc7e3467d2d83539db1b75824e38b7164fbb940162064d7a9b6af50223257 approved-transaction/README.md +6991258799bec838137470e0b9b71ed97169bbdf157896fce3e38b7ebb7d3361 approved-transaction/SHA256SUMS +8182c9bf8b4c6830a2b930698e2e8f5fb7e5ee7849fc8500a8b4813b9292ad51 approved-transaction/backups.txt +f41e0e38632fe2c118cd8d99fc31f9706dd7d6e2479c429580cf067719541850 approved-transaction/comparison-to-221-preview.diff +6a0be15a3e1b6f352cf12761a847ffd5d8d18f6d30ae9064499fd98947d33e37 approved-transaction/host-hooks-inventory.txt +13c60612112a902019a56f23267985eb37ce06159efa2c03a4be5e5d86919090 approved-transaction/installed-versions.txt +d5711f5d3ee9ae38c336b7ae02c036b6927d1e422d72ad8a1f832dae92c41337 approved-transaction/inventory.psv +85d04c74b984200c6d25d85c9473483cd8fc81d53c838643c47aeef2b84d7247 approved-transaction/new-packages.psv +1844d38c86fce118017a4f39472ce58fd0c7281a5856e593a3e64a358c59e992 approved-transaction/normalized-actions.psv +9da051f0cd8b862c9c825dec95e237f7cff2758cb83f81bca549c4c8d3c9ae23 approved-transaction/packaged-hooks.txt +84cf71dd5da459699825c4d25975e15f58bde8fd7cfdb2839bd82441271daaed approved-transaction/scriptlets.txt +f5661dd480a24e6c7814fff9c0765688a4b16309176e97efa5f3e0cb10d6d05d approved-transaction/totals.psv +88252b0d5eaafcdb69a1865b575d44636be203075d996fad2ea6e57b2810150a approved-transaction/upgrades.psv +1a427dc140c990a6d23f1c77eeef4566d193c32d6ed1e6e4d16f7c49d5b9e67f execute-maintenance.sh +e4667c83bd4e9e7de413e48cd16cc2d4b041cb09fbadace5e9f8d74a73a5a2a1 failed-unit-disposition/README.md +e4f82fbefec4bc8c91f473ead9a70d0d0b1a4a14c54a46067127ac05cf4a6dcc failed-unit-disposition/SHA256SUMS +217a0fe59a3bff6c6621bcaac59dcad1c74154486cd7e75e3bba83b6909c29c4 failed-unit-disposition/drkonqi-failed-units.allowlist.psv +f3c3813cf2a7f119f010da956dba1bc25b8986b9e3acaa8eb91fcff95f7ec8c0 post-reboot-validate.sh +0753237d8d42248b18b26cc076782816fbcc5f879133fc0231b6c0902d6f9c4f preflight-read-only.sh 5b88c68da055d3578e8b19fdf7623c3de204d5ef0af4b17e113938d8bd0a2c5e reboot-procedure.md -b6a67a01d4c4f786906d98603ff3075deff29fb6bc4483a642f18a187e4f552a post-reboot-validate.sh 0805397870c66a523f693cfdba33f7b08c94793b62ddadf720ff84c22796ea13 rollback-recovery.md diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/README.md index c6b4cf5..4020a4b 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/README.md +++ b/docs/le-app-database-migration/phase2a-host-maintenance/README.md @@ -88,21 +88,49 @@ The maintenance script refreshes an isolated sync database, normalizes package n The final live `pacman -Syu` must refresh the live sync databases as part of the complete transaction. Pacman cannot expose that post-refresh final interactive transaction for wrapper comparison without either staging a live `pacman -Sy`-only state or replacing the transaction with an unsafe custom workflow. The script therefore requires two exact operator phrases before launch and then requires the operator to review Pacman's final package table at its native interactive `Y/n` prompt. Answer `n` on any difference. This limitation is explicit and is not represented as machine-enforced equality. -## Package-preview evidence +## Approved package-preview evidence -The retained preview is: +The refreshed post-NoMachine preview is: + +```text +/tmp/le-phase2-package-decision-post-nomachine-20260712.1vyjKk +``` + +Its durable normalized baseline and inventories are stored in +`approved-transaction/`. The authoritative transaction is 220 packages: 215 +upgrades, 5 new packages, 1,985.98 MiB download, and +760.30 MiB installed-size +delta. The prior 221-package transaction is superseded. + +Exactly 234 failed `drkonqi-coredump-processor@…` units are reviewed as +nonproduction desktop crash-processing failures. Their checksummed complete +state and invocation-ID fingerprint is under `failed-unit-disposition/`. Any +addition, removal, state or invocation change, checksum failure, or other failed +unit is a hard stop. The scripts do not reset them. `nxserver.service` is not an +expected critical unit; its unused `nomachine` package was removed normally. + +## Snapper readiness history and maintenance gate + +On 2026-07-12 the ordinary empty `/.snapshots` directory was replaced by Btrfs +subvolume ID 300, owned `root:root` mode `0750`. Manual snapshot 1 succeeded and +exact snap-pac testing created and verified snapshots 2 and 3. Btrfs quotas were +intentionally left disabled. + +Preflight verifies that `/.snapshots` remains a Btrfs subvolume. After both +operator approvals and immediately before Pacman, maintenance creates a uniquely +described explicit Snapper snapshot, captures and validates its numeric ID, +verifies its numbered directory and Btrfs snapshot subvolume, and records the ID +in the result. It verifies the snapshot again after Pacman, independently of +snap-pac's return code. Unavailable quota accounting is a warning, not evidence +of snapshot failure. No script enables quotas or runs `snapper setup-quota`. + +The older preview remains at: ```text /tmp/le-phase2-package-decision-20260712.Mn7mIx ``` -Expected report checksum: - -```text -195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d phase2-package-decision-report.md -``` - -The directory occupies approximately 2.4 GiB. Do not delete it yet. It becomes cleanup-eligible only after the operator accepts the maintenance record, the upgrade/reboot succeeds, and retained evidence has been copied to durable approved storage if required. Cleanup is a later explicit command and is not included in any script. +It occupies approximately 2.4 GiB and must not be deleted yet. It is historical, +not the approved baseline. Cleanup remains a later explicit action. ## Hard stop conditions diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/README.md new file mode 100644 index 0000000..94c7ea4 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/README.md @@ -0,0 +1,20 @@ +# Approved Phase 2A transaction baseline + +This checksummed baseline was resolved after removal of `nomachine 9.6.3-1`, +using an isolated Pacman sync database and cache at +`/tmp/le-phase2-package-decision-post-nomachine-20260712.1vyjKk`. The live sync +database and production cache were not modified. + +The modeled complete command is `pacman -Syu --needed rootlesskit slirp4netns +openai-codex`. It resolves 220 packages: 215 upgrades and 5 new packages; +2,082,454,915 bytes (1,985.98 MiB) download; +797,229,927 bytes (+760.30 MiB) +installed-size delta; 147 backup declarations; 8 scriptlets; and 3 packaged +hooks. + +Compared with the old 221-package preview, the sole removed action is +`nomachine 9.6.3-1 -> 9.8.2-1`. Download decreases by 81,814,578 bytes. Installed +delta increases by 403,528 bytes because that upgrade would have reduced size. +The NoMachine scriptlet disappears; hooks and backup declarations are unchanged. + +`normalized-actions.psv` is the machine comparison input. `SHA256SUMS` must +verify before this baseline is accepted. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/SHA256SUMS b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/SHA256SUMS new file mode 100644 index 0000000..df21458 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/SHA256SUMS @@ -0,0 +1,12 @@ +28ebc7e3467d2d83539db1b75824e38b7164fbb940162064d7a9b6af50223257 README.md +8182c9bf8b4c6830a2b930698e2e8f5fb7e5ee7849fc8500a8b4813b9292ad51 backups.txt +f41e0e38632fe2c118cd8d99fc31f9706dd7d6e2479c429580cf067719541850 comparison-to-221-preview.diff +6a0be15a3e1b6f352cf12761a847ffd5d8d18f6d30ae9064499fd98947d33e37 host-hooks-inventory.txt +13c60612112a902019a56f23267985eb37ce06159efa2c03a4be5e5d86919090 installed-versions.txt +d5711f5d3ee9ae38c336b7ae02c036b6927d1e422d72ad8a1f832dae92c41337 inventory.psv +85d04c74b984200c6d25d85c9473483cd8fc81d53c838643c47aeef2b84d7247 new-packages.psv +1844d38c86fce118017a4f39472ce58fd0c7281a5856e593a3e64a358c59e992 normalized-actions.psv +9da051f0cd8b862c9c825dec95e237f7cff2758cb83f81bca549c4c8d3c9ae23 packaged-hooks.txt +84cf71dd5da459699825c4d25975e15f58bde8fd7cfdb2839bd82441271daaed scriptlets.txt +f5661dd480a24e6c7814fff9c0765688a4b16309176e97efa5f3e0cb10d6d05d totals.psv +88252b0d5eaafcdb69a1865b575d44636be203075d996fad2ea6e57b2810150a upgrades.psv diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/backups.txt b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/backups.txt new file mode 100644 index 0000000..932997f --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/backups.txt @@ -0,0 +1,147 @@ +bash-completion-2.18.0-1-any.pkg.tar.zst|etc/bash_completion.d/000_bash_completion_compat.bash +bluez-5.87-2-x86_64.pkg.tar.zst|etc/bluetooth/input.conf +bluez-5.87-2-x86_64.pkg.tar.zst|etc/bluetooth/main.conf +bluez-5.87-2-x86_64.pkg.tar.zst|etc/bluetooth/network.conf +cachyos-ananicy-rules-git-20260711.r2672.gc59d5971-1-any.pkg.tar.zst|etc/ananicy.d/ananicy.conf +clamav-1.5.3-1-x86_64.pkg.tar.zst|etc/clamav/clamav-milter.conf +clamav-1.5.3-1-x86_64.pkg.tar.zst|etc/clamav/clamd.conf +clamav-1.5.3-1-x86_64.pkg.tar.zst|etc/clamav/freshclam.conf +clamav-1.5.3-1-x86_64.pkg.tar.zst|etc/logrotate.d/clamav +geoclue-2.8.2-1-x86_64.pkg.tar.zst|etc/geoclue/geoclue.conf +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/colors.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/delegates.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/log.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/mime.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/policy.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/quantization-table.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/thresholds.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/type-dejavu.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/type-ghostscript.xml +imagemagick-7.1.2.26-2-x86_64.pkg.tar.zst|etc/ImageMagick-7/type.xml +libreoffice-fresh-26.2.4-4-x86_64.pkg.tar.zst|etc/libreoffice/bootstraprc +libreoffice-fresh-26.2.4-4-x86_64.pkg.tar.zst|etc/libreoffice/psprint.conf +libreoffice-fresh-26.2.4-4-x86_64.pkg.tar.zst|etc/libreoffice/sofficerc +libreoffice-fresh-26.2.4-4-x86_64.pkg.tar.zst|etc/profile.d/libreoffice-fresh.csh +libreoffice-fresh-26.2.4-4-x86_64.pkg.tar.zst|etc/profile.d/libreoffice-fresh.sh +libva-2.24.1-1-x86_64.pkg.tar.zst|etc/libva.conf +openssh-10.4p1-2-x86_64.pkg.tar.zst|etc/pam.d/sshd +openssh-10.4p1-2-x86_64.pkg.tar.zst|etc/ssh/ssh_config +openssh-10.4p1-2-x86_64.pkg.tar.zst|etc/ssh/sshd_config +php-8.5.8-1-x86_64.pkg.tar.zst|etc/php/php.ini +php-fpm-8.5.8-1-x86_64.pkg.tar.zst|etc/php/php-fpm.conf +php-fpm-8.5.8-1-x86_64.pkg.tar.zst|etc/php/php-fpm.d/www.conf +samba-2:4.24.4-1-x86_64.pkg.tar.zst|etc/conf.d/samba +samba-2:4.24.4-1-x86_64.pkg.tar.zst|etc/logrotate.d/samba +samba-2:4.24.4-1-x86_64.pkg.tar.zst|etc/pam.d/samba +upower-1.91.3-1-x86_64.pkg.tar.zst|etc/UPower/UPower.conf +webmin-2.651-1-any.pkg.tar.zst|etc/logrotate.d/webmin +webmin-2.651-1-any.pkg.tar.zst|etc/pam.d/webmin +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/acl/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/adsl-client/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/apache/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/at/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/backup-config/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/bacula-backup/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/bandwidth/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/bind8/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/change-user/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-copy/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-cron/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-passwd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-shell/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-software/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-useradmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-usermin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cluster-webmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/cron/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/custom/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/dhcpd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/dovecot/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/exim/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/exports/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/fail2ban/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/fdisk/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/fetchmail/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/filemin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/filter/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/firewall/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/firewall6/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/firewalld/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/fsdump/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/grub2/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/heartbeat/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/htaccess-htpasswd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/idmapd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/init/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/inittab/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/ipsec/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/iscsi-client/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/iscsi-server/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/iscsi-target/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/iscsi-tgtd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/kea-dhcp/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/krb5/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/ldap-client/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/ldap-server/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/ldap-useradmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/logrotate/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/logviewer/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/lpadmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/lvm/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/mailboxes/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/mailcap/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/man/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/miniserv.conf +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/miniserv.users +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/mount/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/mysql/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/net/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/nftables/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/nginx/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/nis/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/openslp/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/pam/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/pap/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/passwd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/phpini/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/postfix/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/postgresql/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/ppp-client/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/pptp-client/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/pptp-server/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/proc/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/procmail/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/proftpd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/qmailadmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/quota/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/raid/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/samba/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/sarg/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/sendmail/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/servers/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/shell/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/shorewall/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/shorewall6/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/smart-status/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/spam/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/squid/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/sshd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/status/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/stunnel/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/syslog/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/system-status/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/systemd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/tcpwrappers/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/time/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/tunnel/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/updown/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/useradmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/usermin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/webalizer/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/webmin.acl +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/webmin/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/webmincron/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/webminlog/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/xinetd/config +webmin-2.651-1-any.pkg.tar.zst|etc/webmin/xterm/config diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/comparison-to-221-preview.diff b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/comparison-to-221-preview.diff new file mode 100644 index 0000000..715a6ae --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/comparison-to-221-preview.diff @@ -0,0 +1,10 @@ +--- previous-221-package-inventory.psv ++++ refreshed-220-package-inventory.psv +@@ -136,7 +136,6 @@ + mesa|1:26.1.3-2|1:26.1.4-1|extra|upgrade|13893205|55149202|55243718 + milou|6.7.1-1|6.7.2-1|extra|upgrade|104229|432387|432387 + node-gyp|13.0.0-1|13.0.1-1|extra|upgrade|1049895|6403165|7063617 +-nomachine|9.6.3-1|9.8.2-1|chaotic-aur|upgrade|81814578|82683022|82279494 + noto-fonts|1:2026.06.01-1|1:2026.07.01-1|extra|upgrade|29203373|111996025|111995861 + npm|11.16.0-1|12.0.0-1|extra|upgrade|1852255|9531710|10250888 + nvidia-settings|610.43.02-1|610.43.03-1|extra|upgrade|804309|1624248|1624248 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/host-hooks-inventory.txt b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/host-hooks-inventory.txt new file mode 100644 index 0000000..78e057d --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/host-hooks-inventory.txt @@ -0,0 +1,954 @@ +/etc/pacman.d/hooks/90-mkinitcpio-install.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/modules/*/vmlinuz +Target = usr/lib/initcpio/* + +[Trigger] +Operation = Install +Operation = Remove +Operation = Upgrade +Type = Package +Target = *-dkms* + +[Action] +Description = Updating linux initcpios... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner kernel +NeedsTargets +/etc/pacman.d/hooks/90-dracut-install.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/dracut/* +Target = usr/lib/firmware/* +Target = usr/src/*/dkms.conf +Target = usr/lib/systemd/systemd +Target = usr/bin/cryptsetup +Target = usr/bin/lvm + +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/modules/*/pkgbase + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = dracut + +[Action] +Description = Updating initramfs with dracut +When = PostTransaction +Exec = /usr/share/libalpm/scripts/dracut-install-garuda +NeedsTargets +/usr/share/libalpm/hooks/05-snap-pac-pre.hook +# snap-pac +# https://github.com/wesbarnett/snap-pac +# Copyright (C) 2016, 2017, 2018 James W. Barnett + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +[Trigger] +Operation = Upgrade +Operation = Install +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Performing snapper pre snapshots for the following configurations... +Depends = snap-pac +When = PreTransaction +Exec = /usr/share/libalpm/scripts/snap-pac pre +NeedsTargets +AbortOnFail +/usr/share/libalpm/hooks/10-linux-modules-post.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Path +Target = usr/lib/modules/*/vmlinuz + +[Action] +Description = Restoring Linux kernel modules... +When = PostTransaction +Depends = coreutils +Depends = rsync +Exec = /bin/sh -xc 'KVER="${KVER:-$(uname -r)}"; if test -e "/usr/lib/modules/backup/${KVER}"; then rsync -AHXal --ignore-existing "/usr/lib/modules/backup/${KVER}" /usr/lib/modules/; fi; rm -rf /usr/lib/modules/backup' +/usr/share/libalpm/hooks/10-linux-modules-pre.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Path +Target = usr/lib/modules/*/vmlinuz + +[Action] +Description = Saving Linux kernel modules... +When = PreTransaction +Depends = rsync +Exec = /bin/sh -c 'KVER="${KVER:-$(uname -r)}"; if test -e "/usr/lib/modules/${KVER}"; then rsync -AHXal --delete-after "/usr/lib/modules/${KVER}" /usr/lib/modules/backup/; fi' +/usr/share/libalpm/hooks/10-snap-pac-removal.hook +# snap-pac +# https://github.com/wesbarnett/snap-pac +# Copyright (C) 2016, 2017, 2018 James W. Barnett + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +[Trigger] +Operation = Remove +Type = Package +Target = snap-pac + +[Action] +Description = You are removing snap-pac. No post transaction snapshots will be taken. +Depends = snap-pac +When = PreTransaction +Exec = /usr/bin/bash -c "rm -f /tmp/snap-pac-pre_*" +/usr/share/libalpm/hooks/40-update-ca-trust.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Path +Target = usr/share/ca-certificates/trust-source/* + +[Action] +Description = Rebuilding certificate stores... +When = PostTransaction +Exec = /usr/bin/update-ca-trust +/usr/share/libalpm/hooks/60-depmod.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/modules/*/ +Target = !usr/lib/modules/*/?* + +[Action] +Description = Updating module dependencies... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/depmod +NeedsTargets +/usr/share/libalpm/hooks/dbus-reload.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = etc/dbus-1/system.d/*.conf +Target = usr/share/dbus-1/system.d/*.conf +Target = usr/share/dbus-1/system-services/*.service + +[Action] +Description = Reloading system bus configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook reload dbus +/usr/share/libalpm/hooks/update-desktop-database.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/applications/*.desktop + +[Action] +Description = Updating the desktop file MIME type cache... +When = PostTransaction +Exec = /usr/bin/update-desktop-database --quiet +/usr/share/libalpm/hooks/zz-snap-pac-post.hook +# snap-pac +# https://github.com/wesbarnett/snap-pac +# Copyright (C) 2016, 2017, 2018 James W. Barnett + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +[Trigger] +Operation = Upgrade +Operation = Install +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Performing snapper post snapshots for the following configurations... +Depends = snap-pac +When = PostTransaction +Exec = /usr/share/libalpm/scripts/snap-pac post +NeedsTargets +/usr/share/libalpm/hooks/dconf-update.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = etc/dconf/db/*.d/ + +[Action] +Description = Updating system dconf databases... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/dconf-update +NeedsTargets +/usr/share/libalpm/hooks/80-cronie.hook +[Trigger] +Operation = Upgrade +Type = Package +Target = glibc + +[Action] +Description = Restarting cronie for libc upgrade... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook restart cronie.service +/usr/share/libalpm/hooks/01-snapshot-reject.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * +Target = !garuda-hooks +Target = !garuda-update + +[Action] +Description = Rejecting pacman transaction if running in snapshot... +When = PreTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner snapshot +AbortOnFail +/usr/share/libalpm/hooks/20-grub-btrfs-config.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = grub-btrfs + +[Action] +Description = Enabling drop-ins for grub-btrfs +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner grub-btrfs-config +/usr/share/libalpm/hooks/20-lsb-release.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = lsb-release + +[Action] +Description = Add Garuda specific configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner lsb-release +/usr/share/libalpm/hooks/20-os-release.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = filesystem + +[Action] +Description = Adding Garuda specific configurations... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner filesystem +/usr/share/libalpm/hooks/80-check-broken-packages.hook +[Trigger] +Operation = Upgrade +Type = Package +Target = * + +[Action] +Description = Checking for package with missing dependencies... +Exec = /usr/bin/check-broken-packages +When = PostTransaction +/usr/share/libalpm/hooks/99-foreign.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Foreign/AUR package notification +When = PostTransaction +Exec = /usr/bin/bash -c "/usr/bin/pacman -Qm || /usr/bin/echo '=> No foreign/AUR packages found.'" +/usr/share/libalpm/hooks/99-grub-install.hook +[Trigger] +Operation = Upgrade +Type = Package +Target = grub + +[Action] +Description = Updating grub binary in EFI +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner grub-update +/usr/share/libalpm/hooks/99-orphans.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Orphaned package notification... +When = PostTransaction +Exec = /usr/bin/bash -c "/usr/bin/pacman -Qtd || /usr/bin/echo '=> No orphans found.'" +#/etc/pacman.d/hooks/orphans.hook +/usr/share/libalpm/hooks/99-pacnew-check.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Checking for .pacnew and .pacsave files... +When = PostTransaction +Exec = /usr/bin/bash -c 'pacfiles=$(pacdiff -o); if [[ -n "$pacfiles" ]]; then echo -e "\e[1m.pac* files found:\e[0m\n$pacfiles\n\e[1mPlease check and merge\e[0m"; fi' +#/etc/pacman.d/hooks/pacnew-check.hook +/usr/share/libalpm/hooks/99-update-grub.hook +[Trigger] +Operation = Install +Operation = Remove +Type = Path +Target = usr/lib/modules/*/vmlinuz + +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = grub + +[Action] +Description = GRUB update after transactions... +When = PostTransaction +Depends = grub +Exec = /bin/sh -c "/usr/bin/update-grub" +/usr/share/libalpm/hooks/garuda-hooks-runner.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = garuda-hooks + +[Action] +Description = Updating Garuda specific hooks... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner +/usr/share/libalpm/hooks/zzz_post.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Syncing all file systems... +Depends = coreutils +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-hooks-runner post +/usr/share/libalpm/hooks/grub-initrd-generation-fix.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = grub +Target = grub-silent +Target = os-prober +Target = os-prober-btrfs + +[Action] +Description = Fix 'grub' and 'os-prober' after upgrading either of them.... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/grub-initrd-generation-fix +/usr/share/libalpm/hooks/30-update-mime-database.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/mime/packages/*.xml + +[Action] +Description = Updating the MIME type database... +When = PostTransaction +Exec = /usr/bin/env PKGSYSTEM_ENABLE_FSYNC=0 /usr/bin/update-mime-database /usr/share/mime +/usr/share/libalpm/hooks/000-garuda-config-agent-post.hook +[Trigger] +Type = Path +Operation = Upgrade +Target = etc/* + +[Action] +Description = Updating configurations with garuda-config-agent... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-config-agent post +NeedsTargets +/usr/share/libalpm/hooks/000-garuda-config-agent-postinst.hook +[Trigger] +Type = Path +Operation = Install +Target = etc/* + +[Action] +Description = Updating configurations with garuda-config-agent... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-config-agent postinst +NeedsTargets +/usr/share/libalpm/hooks/50-garuda-config-agent-pre.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = etc/* + +[Action] +Description = Preparing configurations with garuda-config-agent... +When = PreTransaction +Exec = /usr/share/libalpm/scripts/garuda-config-agent pre +NeedsTargets +/usr/share/libalpm/hooks/00-garuda-migrations.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = garuda-migrations + +[Action] +Description = Applying Garuda Linux migrations... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/garuda-migrations-runner +/usr/share/libalpm/hooks/gtk-query-immodules-3.0.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/gtk-3.0/3.0.0/immodules/*.so + +[Action] +Description = Probing GTK3 input method modules... +When = PostTransaction +Exec = /usr/bin/gtk-query-immodules-3.0 --update-cache +/usr/share/libalpm/hooks/gtk-query-immodules-3.0-32.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib32/gtk-3.0/3.0.0/immodules/*.so + +[Action] +Description = Probing 32-bit GTK3 input method modules... +When = PostTransaction +Exec = /usr/bin/gtk-query-immodules-3.0-32 --update-cache +/usr/share/libalpm/hooks/gio-querymodules.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/gio/modules/*.so + +[Action] +Description = Updating GIO module cache... +When = PostTransaction +Exec = /usr/bin/gio-querymodules /usr/lib/gio/modules +/usr/share/libalpm/hooks/glib-compile-schemas.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/glib-2.0/schemas/*.xml +Target = usr/share/glib-2.0/schemas/*.override + +[Action] +Description = Compiling GSettings XML schema files... +When = PostTransaction +Exec = /usr/bin/glib-compile-schemas /usr/share/glib-2.0/schemas +/usr/share/libalpm/hooks/20-systemd-sysusers.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/sysusers.d/*.conf + +[Action] +Description = Creating system user accounts... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook sysusers +/usr/share/libalpm/hooks/21-systemd-tmpfiles.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/tmpfiles.d/*.conf + +[Action] +Description = Creating temporary files... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook tmpfiles +/usr/share/libalpm/hooks/25-systemd-binfmt.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/binfmt.d/*.conf + +[Action] +Description = Registering binary formats... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook binfmt +/usr/share/libalpm/hooks/25-systemd-catalog.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/systemd/catalog/* + +[Action] +Description = Updating journal message catalog... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook catalog +/usr/share/libalpm/hooks/25-systemd-hwdb.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/udev/hwdb.d/* + +[Action] +Description = Updating udev hardware database... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook hwdb +/usr/share/libalpm/hooks/25-systemd-sysctl.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/sysctl.d/*.conf + +[Action] +Description = Applying kernel sysctl settings... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook sysctl +/usr/share/libalpm/hooks/30-systemd-daemon-reload-system.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/systemd/system/* + +[Action] +Description = Reloading system manager configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook daemon-reload-system +/usr/share/libalpm/hooks/30-systemd-daemon-reload-user.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/systemd/user/* + +[Action] +Description = Reloading user manager configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook daemon-reload-user +/usr/share/libalpm/hooks/35-systemd-enqueue-marked.hook +[Trigger] +Type = Path +Operation = Upgrade +Target = usr/lib/systemd/system/* + +[Action] +Description = Enqueuing marked services... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook enqueue-marked +/usr/share/libalpm/hooks/35-systemd-udev-reload.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/udev/rules.d/* + +[Action] +Description = Reloading device manager configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook udev-reload +/usr/share/libalpm/hooks/35-systemd-update.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/ + +[Action] +Description = Arming ConditionNeedsUpdate... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook update +/usr/share/libalpm/hooks/accounts-daemon-restart.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/accountsservice/interfaces/* + +[Action] +Description = Restarting accounts-daemon... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook restart accounts-daemon.service +/usr/share/libalpm/hooks/40-fontconfig-config.hook +[Trigger] +Type = Path +Operation = Install +Operation = Remove +Target = usr/share/fontconfig/conf.default/* + +[Action] +Description = Updating fontconfig configuration... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/40-fontconfig-config /etc/fonts/conf.d +NeedsTargets +/usr/share/libalpm/hooks/fontconfig.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = etc/fonts/conf.d/* +Target = usr/share/fonts/* +Target = usr/share/fontconfig/conf.avail/* +Target = usr/share/fontconfig/conf.default/* + +[Action] +Description = Updating fontconfig cache... +When = PostTransaction +Exec = /usr/bin/fc-cache -s +/usr/share/libalpm/hooks/gdk-pixbuf-query-loaders.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/gdk-pixbuf-2.0/2.10.0/loaders/*.so + +[Action] +Description = Probing GDK-Pixbuf loader modules... +When = PostTransaction +Exec = /usr/bin/gdk-pixbuf-query-loaders --update-cache +/usr/share/libalpm/hooks/90-update-appstream-cache.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/app-info/* +Target = usr/share/swcatalog/* + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = appstream + +[Action] +Description = Updating the appstream cache... +When = PostTransaction +Exec = /usr/bin/appstreamcli refresh-cache --force +/usr/share/libalpm/hooks/detect-old-perl-modules.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Path +Target = usr/lib/perl5/*/ + +[Action] +Description = Checking for old perl modules... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/detect-old-perl-modules.sh +/usr/share/libalpm/hooks/70-dkms-install.hook +[Trigger] +Operation = Install +Operation = Upgrade +Type = Path +Target = usr/src/*/dkms.conf +Target = usr/lib/modules/*/build/include/ +Target = usr/lib/modules/*/modules.order + +[Action] +Description = Install DKMS modules +Depends = dkms +When = PostTransaction +Exec = /usr/share/libalpm/scripts/dkms install +NeedsTargets +/usr/share/libalpm/hooks/70-dkms-upgrade.hook +[Trigger] +Operation = Upgrade +Type = Path +Target = usr/src/*/dkms.conf +Target = usr/lib/modules/*/build/include/ +Target = usr/lib/modules/*/modules.order + +[Action] +Description = Remove upgraded DKMS modules +Depends = dkms +When = PreTransaction +Exec = /usr/share/libalpm/scripts/dkms -D remove +NeedsTargets +/usr/share/libalpm/hooks/71-dkms-remove.hook +[Trigger] +Operation = Remove +Type = Path +Target = usr/src/*/dkms.conf +Target = usr/lib/modules/*/build/include/ +Target = usr/lib/modules/*/modules.order + +[Action] +Description = Remove DKMS modules +Depends = dkms +When = PreTransaction +Exec = /usr/share/libalpm/scripts/dkms remove +NeedsTargets +/usr/share/libalpm/hooks/60-dracut-remove.hook +[Trigger] +Type = Path +Operation = Remove +Target = usr/lib/modules/*/pkgbase + +[Action] +Description = Removing initramfs with dracut +When = PreTransaction +Exec = /usr/share/libalpm/scripts/dracut-remove +NeedsTargets +/usr/share/libalpm/hooks/90-dracut-install.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/dracut/* +Target = usr/lib/firmware/* +Target = usr/src/*/dkms.conf +Target = usr/lib/systemd/systemd +Target = usr/bin/cryptsetup +Target = usr/bin/lvm + +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/modules/*/vmlinuz +Target = usr/lib/modules/*/pkgbase + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = dracut + +[Action] +Description = Updating initramfs with dracut +When = PostTransaction +Exec = /usr/share/libalpm/scripts/dracut-install +NeedsTargets +/usr/share/libalpm/hooks/gtk-update-icon-cache.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/icons/*/ +Target = !usr/share/icons/*/?* + +[Action] +Description = Updating icon theme caches... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/gtk-update-icon-cache +NeedsTargets +/usr/share/libalpm/hooks/gtk4-querymodules.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/gtk-4.0/4.0.0/*/ + +[Action] +Description = Updating GTK4 module cache... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/gtk4-querymodules +NeedsTargets +/usr/share/libalpm/hooks/gio-querymodules-32.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib32/gio/modules/*.so + +[Action] +Description = Updating 32-bit GIO module cache... +When = PostTransaction +Exec = /usr/bin/gio-querymodules-32 /usr/lib32/gio/modules +/usr/share/libalpm/hooks/fontconfig-32.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = etc/fonts/conf.d/* +Target = usr/share/fonts/* +Target = usr/share/fontconfig/conf.avail/* +Target = usr/share/fontconfig/conf.default/* + +[Action] +Description = Updating 32-bit fontconfig cache... +When = PostTransaction +Exec = /usr/bin/fc-cache-32 -s +/usr/share/libalpm/hooks/gdk-pixbuf-query-loaders-32.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib32/gdk-pixbuf-2.0/2.10.0/loaders/*.so + +[Action] +Description = Probing 32-bit GDK-Pixbuf loader modules... +When = PostTransaction +Exec = /usr/bin/gdk-pixbuf-query-loaders-32 --update-cache +/usr/share/libalpm/hooks/update-vlc-plugin-cache.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/vlc/plugins/* + +[Action] +Description = Updating the vlc plugin cache... +When = PostTransaction +Exec = /usr/lib/vlc/vlc-cache-gen /usr/lib/vlc/plugins +/usr/share/libalpm/hooks/70-openssh-restart-sshd.hook +[Trigger] +Operation = Upgrade +Type = Package +Target = openssh + +[Action] +Description = Restart a running sshd.service +When = PostTransaction +Exec = /usr/share/libalpm/scripts/systemd-hook restart sshd.service +/usr/share/libalpm/hooks/nvidia-ctk-cdi.hook +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = nvidia-utils +Target = nvidia-container-toolkit +Target = opencl-nvidia +Target = egl-gbm +Target = egl-wayland + +[Action] +Description = Regenerate NVIDIA CDI (Container Device Interface) +Exec = /usr/share/libalpm/scripts/nvidia-ctk-cdi +When = PostTransaction +Depends = nvidia-utils +/usr/share/libalpm/hooks/texinfo-install.hook +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/share/info/* + +[Action] +Description = Updating the info directory file... +When = PostTransaction +Exec = /bin/sh -c 'while read -r f; do if test -f "$f"; then install-info "$f" /usr/share/info/dir 2> /dev/null; fi; done' +NeedsTargets +/usr/share/libalpm/hooks/texinfo-remove.hook +[Trigger] +Type = Path +Operation = Remove +Target = usr/share/info/* + +[Action] +Description = Removing old entries from the info directory file... +When = PreTransaction +Exec = /bin/sh -c 'while read -r f; do install-info --delete "$f" /usr/share/info/dir 2> /dev/null; done' +NeedsTargets +/usr/share/libalpm/hooks/unbound-trusted-key.hook +[Trigger] +Type = Path +Target = etc/trusted-key.key +Operation = Install +Operation = Upgrade + +[Action] +Description = Updating trusted-key.key for unbound... +When = PostTransaction +Exec = /bin/cp -f /etc/trusted-key.key /etc/unbound/ +/usr/share/libalpm/hooks/vimdoc.hook +[Trigger] +Operation = Install +Operation = Upgrade +Operation= Remove +Type = Path +Target = usr/share/vim/vimfiles/doc/ + +[Action] +Description = Updating Vim help tags... +Exec = /usr/bin/vim -es --cmd ":helptags /usr/share/vim/vimfiles/doc" --cmd ":q" +When = PostTransaction diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/installed-versions.txt b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/installed-versions.txt new file mode 100644 index 0000000..e8797c8 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/installed-versions.txt @@ -0,0 +1,16 @@ +docker 1:29.6.1-1 +containerd 2.3.2-1 +linux 7.0.14.arch1-1 +linux-headers 7.0.14.arch1-1 +linux-zen 7.0.14.zen1-1 +linux-zen-headers 7.0.14.zen1-1 +openssh 10.3p1-1 +php 8.5.7-1 +php-fpm 8.5.7-1 +php-gd 8.5.7-1 +php-pgsql 8.5.7-1 +php-sodium 8.5.7-1 +nginx 1.30.3-1 +docker-compose 5.1.4-1 +systemd 261.1-1 +mariadb-libs 12.3.2-2 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/inventory.psv b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/inventory.psv new file mode 100644 index 0000000..2408b2f --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/inventory.psv @@ -0,0 +1,220 @@ +tzdata|2026b-1|2026c-1|core|upgrade|312226|1721583|1714499 +libgcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|81875|186076|185836 +libstdc++|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|883329|2940043|2940043 +libevent|2.1.12-5|2.1.13-1|core|upgrade|298462|1174357|1272640 +rootlesskit|-|3.0.1-1|extra|new|6893581|0|31928317 +libffi|3.6.0-1|3.7.1-1|core|upgrade|54112|106541|113669 +libslirp|-|4.9.3-1|extra|new|72785|0|166539 +slirp4netns|-|1.3.4-1|extra|new|29689|0|69291 +openai-codex|-|0.144.1-2|extra|new|85177155|0|352545716 +hwdata|0.408-1|0.409-1|core|upgrade|1772670|10424361|10474588 +libssh2|1.11.1-5|1.11.1-6|core|upgrade|265565|514626|514626 +leancrypto|1.7.2-1|1.8.0-1|core|upgrade|1487902|6493177|6622604 +accountsservice|26.26.9-1|26.27.3-1|extra|upgrade|173172|991953|991953 +pinentry|1.3.2-2|1.3.3-1|core|upgrade|172284|784782|692067 +gnupg|2.4.9-1|2.4.9-2|core|upgrade|3028428|10781728|10912940 +gpgme|2.1.1-1|2.1.2-1|core|upgrade|339305|809005|809006 +libgomp|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|236396|447361|447361 +archlinux-keyring|20260612-1|20260707.1-1|core|upgrade|1270069|1806591|1811296 +libjpeg-turbo|3.1.4.1-1|3.2.0-2|extra|upgrade|615715|2547213|2778875 +llvm-libs|22.1.6-1|22.1.8-1|extra|upgrade|42954242|171669045|171669045 +mesa|1:26.1.3-2|1:26.1.4-1|extra|upgrade|13893205|55149202|55243718 +liburing|2.14-1|2.15-1|extra|upgrade|272523|470224|489708 +shared-mime-info|2.4-3|2.5.1-1|extra|upgrade|783748|4798765|6111065 +libasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|533961|1974116|1974116 +libhwasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|281807|757548|757548 +liblsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|233779|573212|573212 +libtsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|470867|1523548|1523548 +libubsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|211584|520076|520076 +libatomic|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|14195|38092|38092 +libgfortran|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|948716|3684588|3684588 +libobjc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|38430|94212|94212 +libquadmath|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|170420|316008|316008 +gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|2700||0 +libjxl|0.11.2-2|0.12.0-1|extra|upgrade|1947638|10121485|7111117 +libva|2.23.0-2|2.24.1-1|extra|upgrade|210646|1034965|1043980 +libtiff|4.7.1-2|4.7.2-1|extra|upgrade|422908|1316209|1387341 +bluez-libs|5.86-6|5.87-2|extra|upgrade|95069|324535|324595 +glycin|2.1.5-1|2.1.5-2|extra|upgrade|3962413|18334284|18338616 +sdl3|3.4.10-1|3.4.12-1|extra|upgrade|1791106|7290426|7303530 +ffmpeg|2:8.1.2-7|2:8.1.2-10|extra|upgrade|15546334|50350499|50309539 +poppler|26.06.0-1|26.07.0-1|extra|upgrade|1893273|7090429|7070570 +poppler-qt6|26.06.0-1|26.07.0-1|extra|upgrade|274672|915373|915373 +upower|1.91.2-1|1.91.3-1|extra|upgrade|176172|883601|891933 +ark|26.04.2-1|26.04.3-1|extra|upgrade|1505995|5298413|5298715 +kdecoration|6.7.1-1|6.7.2-1|extra|upgrade|100682|401077|401077 +aurorae|6.7.1-1|6.7.2-1|extra|upgrade|159179|647303|647303 +baloo-widgets|26.04.2-2|26.04.3-1|extra|upgrade|153469|526624|526604 +bash-completion|2.17.0-3|2.18.0-1|extra|upgrade|230258|1060727|1043228 +plasma-activities|6.7.1-1|6.7.2-1|extra|upgrade|128347|462452|462452 +libplasma|6.7.1-1|6.7.2-1|extra|upgrade|2636197|7193216|7193398 +bluez|5.86-6|5.87-2|extra|upgrade|696024|1693782|1812713 +bluedevil|1:6.7.1-1|1:6.7.2-1|extra|upgrade|683660|2708069|2708074 +bluez-hid2hci|5.86-6|5.87-2|extra|upgrade|8923|16580|16578 +bluez-utils|5.86-6|5.87-2|extra|upgrade|1169283|3746338|3865141 +breeze-cursors|6.7.1-1|6.7.2-1|extra|upgrade|1301676|30673949|30673949 +breeze|6.7.1-1|6.7.2-1|extra|upgrade|42064099|43158908|43159003 +breeze-gtk|6.7.1-1|6.7.2-1|extra|upgrade|196883|1213077|1213077 +c-ares|1.34.6-1|1.34.8-1|extra|upgrade|244660|533090|555071 +cachyos-ananicy-rules-git|20260630.r2628.gebf4fa42-1|20260711.r2672.gc59d5971-1|chaotic-aur|upgrade|425439|1894425|1982152 +clamav|1.5.2-2|1.5.3-1|extra|upgrade|32136399|187320189|190095084 +libisl|0.27-1|0.28-1|core|upgrade|980886|6034037|6191080 +gcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|60610338|229943007|230854455 +compiler-rt|22.1.6-1|22.1.8-1|extra|upgrade|4824931|57605800|57605800 +clang|22.1.6-1|22.1.8-1|extra|upgrade|59294452|266782095|266782637 +cmake|4.3.4-1|4.4.0-1|extra|upgrade|15655871|102747843|106399125 +signon-kwallet-extension|26.04.2-1|26.04.3-1|extra|upgrade|15636|35296|35296 +noto-fonts|1:2026.06.01-1|1:2026.07.01-1|extra|upgrade|29203373|111996025|111995861 +kaccounts-integration|26.04.2-1|26.04.3-1|extra|upgrade|156244|637488|638150 +discover|6.7.1-1|6.7.2-1|extra|upgrade|1631911|7642315|7643332 +docker-compose|5.1.4-1|5.3.1-1|extra|upgrade|8454742|29675029|29355637 +libkexiv2|26.04.2-1|26.04.3-1|extra|upgrade|162511|491526|487430 +ldb|2:4.24.3-1|2:4.24.4-1|extra|upgrade|499721|2274504|2274502 +libwbclient|2:4.24.3-1|2:4.24.4-1|extra|upgrade|38772|130413|130413 +smbclient|2:4.24.3-1|2:4.24.4-1|extra|upgrade|7684453|30393785|30385773 +kio-extras|26.04.2-1|26.04.3-1|extra|upgrade|1984190|7700750|7697171 +dolphin|26.04.2-1|26.04.3-1|extra|upgrade|5042500|16239749|16239807 +python-sentry_sdk|2.63.0-1|2.64.0-1|extra|upgrade|977194|6876700|6923234 +drkonqi|6.7.1-1|6.7.2-1|extra|upgrade|737195|3551937|3551937 +electron42|42.3.0-1|42.6.1-1|extra|upgrade|97773568|314483012|345325566 +ethtool|1:7.0-1|1:7.1-1|extra|upgrade|308249|976786|989761 +ostree|2025.7-3|2026.2-1|extra|upgrade|857240|4577682|4573416 +gstreamer|1.28.4-2|1.28.5-1|extra|upgrade|2215827|12484778|12495257 +gst-plugins-base-libs|1.28.4-2|1.28.5-1|extra|upgrade|2570881|13461176|13465336 +flatpak-kcm|6.7.1-1|6.7.2-1|extra|upgrade|218308|793524|793862 +freerdp|2:3.27.1-1|2:3.28.0-1|extra|upgrade|2824842|9579223|9710525 +fzf|0.73.1-1|0.74.0-1|extra|upgrade|1750795|5033461|5105400 +electron43|-|43.1.0-1|extra|new|99272635|0|348715206 +garuda-toolbox|5.3.2-1|5.3.2-2|garuda|upgrade|49300621|117153640|117153640 +geoclue|2.8.1-1|2.8.2-1|extra|upgrade|222462|1486901|1498397 +go|2:1.26.4-1|2:1.26.5-1|extra|upgrade|41137734|226068739|226127343 +gst-plugins-bad-libs|1.28.4-2|1.28.5-1|extra|upgrade|3336254|15439610|15443706 +gst-plugins-base|1.28.4-2|1.28.5-1|extra|upgrade|222885|659712|659712 +libkdcraw|26.04.2-1|26.04.3-1|extra|upgrade|45262|139093|139093 +gwenview|26.04.2-1|26.04.3-1|extra|upgrade|6926077|11945408|11947979 +imagemagick|7.1.2.26-1|7.1.2.26-2|extra|upgrade|9341553|25538545|25538346 +procps-ng|4.0.6-2|4.0.6-3|core|upgrade|1022125|2673185|2673185 +inxi|3.3.40.1-1|3.3.41.1-1|extra|upgrade|363272|1420915|1422148 +kactivitymanagerd|6.7.1-1|6.7.2-1|extra|upgrade|202599|770328|770328 +kde-cli-tools|6.7.1-1|6.7.2-1|extra|upgrade|930408|3992616|3992616 +xsettingsd|1.0.2-3|1.0.4-1|extra|upgrade|30563|68794|73189 +kde-gtk-config|6.7.1-1|6.7.2-1|extra|upgrade|90823|331955|331955 +kglobalacceld|6.7.1-1|6.7.2-1|extra|upgrade|135756|416155|416155 +knighttime|6.7.1-1|6.7.2-1|extra|upgrade|67737|198688|198688 +layer-shell-qt|6.7.1-1|6.7.2-1|extra|upgrade|38535|145672|145672 +libkscreen|6.7.1-1|6.7.2-1|extra|upgrade|360141|1218759|1218759 +kscreenlocker|6.7.1-1|6.7.2-1|extra|upgrade|269833|1072950|1072950 +kwayland|6.7.1-1|6.7.2-1|extra|upgrade|251494|1279721|1279721 +milou|6.7.1-1|6.7.2-1|extra|upgrade|104229|432387|432387 +kwin|6.7.1-1|6.7.2-1|extra|upgrade|11095191|33501293|33524016 +kpipewire|6.7.1-1|6.7.2-1|extra|upgrade|198596|657891|653795 +libksysguard|6.7.1-1|6.7.2-1|extra|upgrade|720832|3607942|3607977 +ksystemstats|6.7.1-1|6.7.2-1|extra|upgrade|301054|1672961|1665151 +ocean-sound-theme|6.7.1-1|6.7.2-1|extra|upgrade|2026004|2175618|2175618 +plasma-activities-stats|6.7.1-1|6.7.2-1|extra|upgrade|95452|299817|299817 +xorg-xrdb|1.2.2-2|1.2.3-1|extra|upgrade|21227|41872|41949 +libxfont2|2.0.7-1|2.0.8-1|extra|upgrade|123151|237487|253871 +xorg-server-common|21.1.23-1|21.1.24-1|extra|upgrade|28305|130326|130326 +xorg-xwayland|24.1.12-1|24.1.13-1|extra|upgrade|1031315|2459481|2459481 +qqc2-breeze-style|6.7.1-1|6.7.2-1|extra|upgrade|470391|2871912|2871912 +xdg-desktop-portal-kde|6.7.1-1|6.7.2-1|extra|upgrade|689386|2791697|2792353 +plasma-integration|6.7.1-1|6.7.2-1|extra|upgrade|134710|425855|425855 +zxing-cpp|3.0.2-1|3.1.0-1|extra|upgrade|862154|1765316|1915890 +plasma-workspace|6.7.1-1|6.7.2-1|extra|upgrade|22146328|58197663|58253276 +kdeplasma-addons|6.7.1-1|6.7.2-1|extra|upgrade|3693743|19412292|19416224 +kimageformats|6.27.0-1|6.27.0-2|extra|upgrade|663131|3052500|3048404 +systemsettings|6.7.1-1|6.7.2-1|extra|upgrade|381269|1201439|1201439 +kinfocenter|6.7.1-1|6.7.2-1|extra|upgrade|984967|3250792|3255523 +kitty-terminfo|0.47.1-1|0.47.4-1|extra|upgrade|4718|3721|3721 +kitty-shell-integration|0.47.1-1|0.47.4-1|extra|upgrade|26986|90651|90651 +kitty|0.47.1-1|0.47.4-1|extra|upgrade|19009603|67770526|68515011 +kmenuedit|6.7.1-1|6.7.2-1|extra|upgrade|1067024|2028044|2028044 +konsole|26.04.2-1|26.04.3-1|extra|upgrade|2333435|10237598|10238056 +opencv|4.13.0-11|5.0.0-6|extra|upgrade|35512409|110222236|118294340 +kquickimageeditor|0.6.2-1|0.6.2.1-2|extra|upgrade|321027|1343696|1343696 +qtkeychain-qt6|0.16.0-1|0.17.0-1|extra|upgrade|76490|270934|278370 +krdp|6.7.1-1|6.7.2-1|extra|upgrade|227269|847069|851165 +plasma5support|6.7.1-1|6.7.2-1|extra|upgrade|1251965|9592075|9588001 +kscreen|6.7.1-1|6.7.2-1|extra|upgrade|1942241|3546789|3547398 +ksshaskpass|6.7.1-1|6.7.2-1|extra|upgrade|37936|133293|133293 +kwallet-pam|6.7.1-1|6.7.2-1|extra|upgrade|13402|28402|28402 +kwrited|6.7.1-1|6.7.2-1|extra|upgrade|18885|44940|44940 +lib32-gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|11344430|47147884|47147884 +lib32-brotli|1.1.0-1|1.2.0-2|multilib|upgrade|348002|898138|898295 +lib32-libdatrie|0.2.13-3|0.2.14-1|multilib|upgrade|15318|26141|26141 +lib32-libffi|3.6.0-1|3.7.1-1|multilib|upgrade|20827|43795|43795 +lib32-libjpeg-turbo|3.1.4.1-1|3.2.0-1|multilib|upgrade|389607|1566179|1734544 +lib32-libssh2|1.11.1-3|1.11.1-4|multilib|upgrade|122812|303297|303297 +lib32-libthai|0.1.29-3|0.1.30-1|multilib|upgrade|17971|44716|43692 +lib32-libtiff|4.7.1-1|4.7.2-1|multilib|upgrade|228125|687551|695810 +lib32-llvm-libs|1:22.1.6-1|1:22.1.8-1|multilib|upgrade|44333543|176603193|176603193 +lib32-mesa|1:26.1.3-2|1:26.1.4-1|multilib|upgrade|13762370|55678468|55690756 +lib32-pam|1.7.1-1|1.7.2-1|multilib|upgrade|166274|793477|809861 +lib32-sdl3|3.4.10-1|3.4.12-1|multilib|upgrade|1243526|3559753|3563785 +vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1|extra|upgrade|56381|130452|130452 +lib32-vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1|multilib|upgrade|54765|127580|127580 +vulkan-intel|1:26.1.3-2|1:26.1.4-1|extra|upgrade|5079461|44221720|44357080 +lib32-vulkan-intel|1:26.1.3-2|1:26.1.4-1|multilib|upgrade|4987152|44165704|44239560 +vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1|extra|upgrade|468155|2253012|2322644 +lib32-vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1|multilib|upgrade|465274|2313704|2391528 +vulkan-radeon|1:26.1.3-2|1:26.1.4-1|extra|upgrade|3548340|18409113|18413581 +lib32-vulkan-radeon|1:26.1.3-2|1:26.1.4-1|multilib|upgrade|3540574|18437520|18437584 +libgccjit|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|core|upgrade|12882406|41430018|41430018 +librevenge|0.0.5-4|0.0.6-1|extra|upgrade|619036|9730454|9748390 +libstaroffice|0.0.7-5|0.0.8-1|extra|upgrade|793891|2363788|2396628 +libreoffice-fresh|26.2.4-3|26.2.4-4|extra|upgrade|154317022|441950779|441905723 +libxnvctrl|610.43.02-1|610.43.03-1|extra|upgrade|78002|468890|468890 +linux|7.0.14.arch1-1|7.1.3.arch1-2|core|upgrade|154459082|154614536|154831841 +linux-headers|7.0.14.arch1-1|7.1.3.arch1-2|core|upgrade|64264745|291805361|295927152 +linux-zen|7.0.14.zen1-1|7.1.3.zen1-2|extra|upgrade|160133452|160263553|160501012 +linux-zen-headers|7.0.14.zen1-1|7.1.3.zen1-2|extra|upgrade|65405662|295536438|299659274 +llvm|22.1.6-1|22.1.8-1|extra|upgrade|24306277|137300369|137300419 +luajit|2.1.1780076327+b925b3e-1|2.1.1783773675+3c4f9fe-1|extra|upgrade|344119|854974|854974 +mariadb-libs|12.3.2-2|12.3.2-3|extra|upgrade|6373469|24801390|24789104 +node-gyp|13.0.0-1|13.0.1-1|extra|upgrade|1049895|6403165|7063617 +npm|11.16.0-1|12.0.0-1|extra|upgrade|1852255|9531710|10250888 +nvidia-settings|610.43.02-1|610.43.03-1|extra|upgrade|804309|1624248|1624248 +openssh|10.3p1-1|10.4p1-2|core|upgrade|1485983|5721620|7054017 +oxygen-cursors|6.7.1-1|6.7.2-1|extra|upgrade|1063147|17949898|17949898 +oxygen|6.7.1-1|6.7.2-1|extra|upgrade|31583476|35956530|35956560 +oxygen-sounds|6.7.1-1|6.7.2-1|extra|upgrade|1936125|2165836|2165836 +perl-dbi|1.649-1|1.650-1|extra|upgrade|973765|2751714|2754728 +perl-http-date|6.07-1|6.08-1|extra|upgrade|10719|15592|16625 +php|8.5.7-1|8.5.8-1|extra|upgrade|5930025|41065264|41062319 +php-fpm|8.5.7-1|8.5.8-1|extra|upgrade|4523580|30222807|30222807 +php-gd|8.5.7-1|8.5.8-1|extra|upgrade|40448|133240|133240 +php-pgsql|8.5.7-1|8.5.8-1|extra|upgrade|80148|233728|233728 +php-sodium|8.5.7-1|8.5.8-1|extra|upgrade|28573|112768|112768 +plasma-browser-integration|6.7.1-1|6.7.2-1|extra|upgrade|191209|629051|629051 +polkit-kde-agent|6.7.1-1|6.7.2-1|extra|upgrade|75915|255832|255832 +powerdevil|6.7.1-1|6.7.2-1|extra|upgrade|1670507|6455597|6460249 +plasma-desktop|6.7.1-1|6.7.2-1|extra|upgrade|19090969|41914158|41925994 +plasma-disks|6.7.1-1|6.7.2-1|extra|upgrade|164187|639736|639736 +plasma-firewall|6.7.1-1|6.7.2-1|extra|upgrade|515921|1404582|1404582 +plasma-keyboard|6.7.1-1|6.7.2-1|extra|upgrade|304349|1616171|1616220 +plasma-login-manager|6.7.1-1|6.7.2-1|extra|upgrade|436905|1768727|1768728 +plasma-nm|6.7.1-1|6.7.2-1|extra|upgrade|2131144|13808938|13815045 +plasma-pa|6.7.1-1|6.7.2-1|extra|upgrade|510561|2202142|2202148 +plasma-systemmonitor|6.7.1-1|6.7.2-1|extra|upgrade|540594|2737379|2738210 +plasma-thunderbolt|6.7.1-1|6.7.2-1|extra|upgrade|146457|564048|564048 +plasma-vault|6.7.1-1|6.7.2-1|extra|upgrade|335384|1291838|1296901 +plasma-welcome|6.7.1-1|6.7.2-1|extra|upgrade|1071501|3662215|3661774 +plasma-workspace-wallpapers|6.7.1-1|6.7.2-1|extra|upgrade|266511331|267444293|267444361 +print-manager|1:6.7.1-1|1:6.7.2-1|extra|upgrade|597046|2833359|2839131 +python-cffi|2.0.0-2|2.1.0-1|extra|upgrade|306534|1447769|1470834 +python-numpy|2.5.0-1|2.5.1-1|extra|upgrade|8412761|51429283|51485731 +python-pillow|12.2.0-1|12.3.0-1|extra|upgrade|977436|5079139|5116215 +python-setuptools|1:82.0.1-1|1:83.0.0-1|extra|upgrade|1236098|7709707|7725238 +python-typing_extensions|4.15.0-3|4.16.0-1|extra|upgrade|98775|542558|554817 +samba|2:4.24.3-1|2:4.24.4-1|extra|upgrade|9118686|66621565|66622194 +xorg-server|21.1.23-1|21.1.24-1|extra|upgrade|1612671|4045291|4045283 +sddm-kcm|6.7.1-1|6.7.2-1|extra|upgrade|162397|622433|622433 +spectacle|1:6.7.1-1|1:6.7.2-2|extra|upgrade|2182734|6448006|6456051 +tmux|3.7-1|3.7_b-1|extra|upgrade|541272|1274062|1269966 +vulkan-swrast|1:26.1.3-2|1:26.1.4-1|extra|upgrade|2411393|14666799|14679247 +webkit2gtk-4.1|2.52.4-1|2.52.5-1|extra|upgrade|38308819|139540041|141793533 +webmin|2.610-1|2.651-1|chaotic-aur|upgrade|32160470|88877941|91454445 +weechat|4.9.1-1|4.9.3-1|extra|upgrade|3060878|30352238|30353484 +xorg-xset|1.2.5-2|1.2.6-1|extra|upgrade|20082|41029|41107 +xorg-xsetroot|1.1.3-2|1.1.4-1|extra|upgrade|11990|21845|26026 +yad|14.2-1|15.0-1|extra|upgrade|210896|613543|643911 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/new-packages.psv b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/new-packages.psv new file mode 100644 index 0000000..46218b3 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/new-packages.psv @@ -0,0 +1,5 @@ +rootlesskit|3.0.1-1 +libslirp|4.9.3-1 +slirp4netns|1.3.4-1 +openai-codex|0.144.1-2 +electron43|43.1.0-1 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/normalized-actions.psv b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/normalized-actions.psv new file mode 100644 index 0000000..483426c --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/normalized-actions.psv @@ -0,0 +1,220 @@ +accountsservice|26.26.9-1|26.27.3-1|upgrade +archlinux-keyring|20260612-1|20260707.1-1|upgrade +ark|26.04.2-1|26.04.3-1|upgrade +aurorae|6.7.1-1|6.7.2-1|upgrade +baloo-widgets|26.04.2-2|26.04.3-1|upgrade +bash-completion|2.17.0-3|2.18.0-1|upgrade +bluedevil|1:6.7.1-1|1:6.7.2-1|upgrade +bluez-hid2hci|5.86-6|5.87-2|upgrade +bluez-libs|5.86-6|5.87-2|upgrade +bluez-utils|5.86-6|5.87-2|upgrade +bluez|5.86-6|5.87-2|upgrade +breeze-cursors|6.7.1-1|6.7.2-1|upgrade +breeze-gtk|6.7.1-1|6.7.2-1|upgrade +breeze|6.7.1-1|6.7.2-1|upgrade +c-ares|1.34.6-1|1.34.8-1|upgrade +cachyos-ananicy-rules-git|20260630.r2628.gebf4fa42-1|20260711.r2672.gc59d5971-1|upgrade +clamav|1.5.2-2|1.5.3-1|upgrade +clang|22.1.6-1|22.1.8-1|upgrade +cmake|4.3.4-1|4.4.0-1|upgrade +compiler-rt|22.1.6-1|22.1.8-1|upgrade +discover|6.7.1-1|6.7.2-1|upgrade +docker-compose|5.1.4-1|5.3.1-1|upgrade +dolphin|26.04.2-1|26.04.3-1|upgrade +drkonqi|6.7.1-1|6.7.2-1|upgrade +electron42|42.3.0-1|42.6.1-1|upgrade +electron43|-|43.1.0-1|new +ethtool|1:7.0-1|1:7.1-1|upgrade +ffmpeg|2:8.1.2-7|2:8.1.2-10|upgrade +flatpak-kcm|6.7.1-1|6.7.2-1|upgrade +freerdp|2:3.27.1-1|2:3.28.0-1|upgrade +fzf|0.73.1-1|0.74.0-1|upgrade +garuda-toolbox|5.3.2-1|5.3.2-2|upgrade +gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +gcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +geoclue|2.8.1-1|2.8.2-1|upgrade +glycin|2.1.5-1|2.1.5-2|upgrade +gnupg|2.4.9-1|2.4.9-2|upgrade +go|2:1.26.4-1|2:1.26.5-1|upgrade +gpgme|2.1.1-1|2.1.2-1|upgrade +gst-plugins-bad-libs|1.28.4-2|1.28.5-1|upgrade +gst-plugins-base-libs|1.28.4-2|1.28.5-1|upgrade +gst-plugins-base|1.28.4-2|1.28.5-1|upgrade +gstreamer|1.28.4-2|1.28.5-1|upgrade +gwenview|26.04.2-1|26.04.3-1|upgrade +hwdata|0.408-1|0.409-1|upgrade +imagemagick|7.1.2.26-1|7.1.2.26-2|upgrade +inxi|3.3.40.1-1|3.3.41.1-1|upgrade +kaccounts-integration|26.04.2-1|26.04.3-1|upgrade +kactivitymanagerd|6.7.1-1|6.7.2-1|upgrade +kde-cli-tools|6.7.1-1|6.7.2-1|upgrade +kde-gtk-config|6.7.1-1|6.7.2-1|upgrade +kdecoration|6.7.1-1|6.7.2-1|upgrade +kdeplasma-addons|6.7.1-1|6.7.2-1|upgrade +kglobalacceld|6.7.1-1|6.7.2-1|upgrade +kimageformats|6.27.0-1|6.27.0-2|upgrade +kinfocenter|6.7.1-1|6.7.2-1|upgrade +kio-extras|26.04.2-1|26.04.3-1|upgrade +kitty-shell-integration|0.47.1-1|0.47.4-1|upgrade +kitty-terminfo|0.47.1-1|0.47.4-1|upgrade +kitty|0.47.1-1|0.47.4-1|upgrade +kmenuedit|6.7.1-1|6.7.2-1|upgrade +knighttime|6.7.1-1|6.7.2-1|upgrade +konsole|26.04.2-1|26.04.3-1|upgrade +kpipewire|6.7.1-1|6.7.2-1|upgrade +kquickimageeditor|0.6.2-1|0.6.2.1-2|upgrade +krdp|6.7.1-1|6.7.2-1|upgrade +kscreenlocker|6.7.1-1|6.7.2-1|upgrade +kscreen|6.7.1-1|6.7.2-1|upgrade +ksshaskpass|6.7.1-1|6.7.2-1|upgrade +ksystemstats|6.7.1-1|6.7.2-1|upgrade +kwallet-pam|6.7.1-1|6.7.2-1|upgrade +kwayland|6.7.1-1|6.7.2-1|upgrade +kwin|6.7.1-1|6.7.2-1|upgrade +kwrited|6.7.1-1|6.7.2-1|upgrade +layer-shell-qt|6.7.1-1|6.7.2-1|upgrade +ldb|2:4.24.3-1|2:4.24.4-1|upgrade +leancrypto|1.7.2-1|1.8.0-1|upgrade +lib32-brotli|1.1.0-1|1.2.0-2|upgrade +lib32-gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +lib32-libdatrie|0.2.13-3|0.2.14-1|upgrade +lib32-libffi|3.6.0-1|3.7.1-1|upgrade +lib32-libjpeg-turbo|3.1.4.1-1|3.2.0-1|upgrade +lib32-libssh2|1.11.1-3|1.11.1-4|upgrade +lib32-libthai|0.1.29-3|0.1.30-1|upgrade +lib32-libtiff|4.7.1-1|4.7.2-1|upgrade +lib32-llvm-libs|1:22.1.6-1|1:22.1.8-1|upgrade +lib32-mesa|1:26.1.3-2|1:26.1.4-1|upgrade +lib32-pam|1.7.1-1|1.7.2-1|upgrade +lib32-sdl3|3.4.10-1|3.4.12-1|upgrade +lib32-vulkan-intel|1:26.1.3-2|1:26.1.4-1|upgrade +lib32-vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1|upgrade +lib32-vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1|upgrade +lib32-vulkan-radeon|1:26.1.3-2|1:26.1.4-1|upgrade +libasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libatomic|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libevent|2.1.12-5|2.1.13-1|upgrade +libffi|3.6.0-1|3.7.1-1|upgrade +libgccjit|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libgcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libgfortran|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libgomp|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libhwasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libisl|0.27-1|0.28-1|upgrade +libjpeg-turbo|3.1.4.1-1|3.2.0-2|upgrade +libjxl|0.11.2-2|0.12.0-1|upgrade +libkdcraw|26.04.2-1|26.04.3-1|upgrade +libkexiv2|26.04.2-1|26.04.3-1|upgrade +libkscreen|6.7.1-1|6.7.2-1|upgrade +libksysguard|6.7.1-1|6.7.2-1|upgrade +liblsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libobjc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libplasma|6.7.1-1|6.7.2-1|upgrade +libquadmath|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libreoffice-fresh|26.2.4-3|26.2.4-4|upgrade +librevenge|0.0.5-4|0.0.6-1|upgrade +libslirp|-|4.9.3-1|new +libssh2|1.11.1-5|1.11.1-6|upgrade +libstaroffice|0.0.7-5|0.0.8-1|upgrade +libstdc++|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libtiff|4.7.1-2|4.7.2-1|upgrade +libtsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +libubsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3|upgrade +liburing|2.14-1|2.15-1|upgrade +libva|2.23.0-2|2.24.1-1|upgrade +libwbclient|2:4.24.3-1|2:4.24.4-1|upgrade +libxfont2|2.0.7-1|2.0.8-1|upgrade +libxnvctrl|610.43.02-1|610.43.03-1|upgrade +linux-headers|7.0.14.arch1-1|7.1.3.arch1-2|upgrade +linux-zen-headers|7.0.14.zen1-1|7.1.3.zen1-2|upgrade +linux-zen|7.0.14.zen1-1|7.1.3.zen1-2|upgrade +linux|7.0.14.arch1-1|7.1.3.arch1-2|upgrade +llvm-libs|22.1.6-1|22.1.8-1|upgrade +llvm|22.1.6-1|22.1.8-1|upgrade +luajit|2.1.1780076327+b925b3e-1|2.1.1783773675+3c4f9fe-1|upgrade +mariadb-libs|12.3.2-2|12.3.2-3|upgrade +mesa|1:26.1.3-2|1:26.1.4-1|upgrade +milou|6.7.1-1|6.7.2-1|upgrade +node-gyp|13.0.0-1|13.0.1-1|upgrade +noto-fonts|1:2026.06.01-1|1:2026.07.01-1|upgrade +npm|11.16.0-1|12.0.0-1|upgrade +nvidia-settings|610.43.02-1|610.43.03-1|upgrade +ocean-sound-theme|6.7.1-1|6.7.2-1|upgrade +openai-codex|-|0.144.1-2|new +opencv|4.13.0-11|5.0.0-6|upgrade +openssh|10.3p1-1|10.4p1-2|upgrade +ostree|2025.7-3|2026.2-1|upgrade +oxygen-cursors|6.7.1-1|6.7.2-1|upgrade +oxygen-sounds|6.7.1-1|6.7.2-1|upgrade +oxygen|6.7.1-1|6.7.2-1|upgrade +perl-dbi|1.649-1|1.650-1|upgrade +perl-http-date|6.07-1|6.08-1|upgrade +php-fpm|8.5.7-1|8.5.8-1|upgrade +php-gd|8.5.7-1|8.5.8-1|upgrade +php-pgsql|8.5.7-1|8.5.8-1|upgrade +php-sodium|8.5.7-1|8.5.8-1|upgrade +php|8.5.7-1|8.5.8-1|upgrade +pinentry|1.3.2-2|1.3.3-1|upgrade +plasma-activities-stats|6.7.1-1|6.7.2-1|upgrade +plasma-activities|6.7.1-1|6.7.2-1|upgrade +plasma-browser-integration|6.7.1-1|6.7.2-1|upgrade +plasma-desktop|6.7.1-1|6.7.2-1|upgrade +plasma-disks|6.7.1-1|6.7.2-1|upgrade +plasma-firewall|6.7.1-1|6.7.2-1|upgrade +plasma-integration|6.7.1-1|6.7.2-1|upgrade +plasma-keyboard|6.7.1-1|6.7.2-1|upgrade +plasma-login-manager|6.7.1-1|6.7.2-1|upgrade +plasma-nm|6.7.1-1|6.7.2-1|upgrade +plasma-pa|6.7.1-1|6.7.2-1|upgrade +plasma-systemmonitor|6.7.1-1|6.7.2-1|upgrade +plasma-thunderbolt|6.7.1-1|6.7.2-1|upgrade +plasma-vault|6.7.1-1|6.7.2-1|upgrade +plasma-welcome|6.7.1-1|6.7.2-1|upgrade +plasma-workspace-wallpapers|6.7.1-1|6.7.2-1|upgrade +plasma-workspace|6.7.1-1|6.7.2-1|upgrade +plasma5support|6.7.1-1|6.7.2-1|upgrade +polkit-kde-agent|6.7.1-1|6.7.2-1|upgrade +poppler-qt6|26.06.0-1|26.07.0-1|upgrade +poppler|26.06.0-1|26.07.0-1|upgrade +powerdevil|6.7.1-1|6.7.2-1|upgrade +print-manager|1:6.7.1-1|1:6.7.2-1|upgrade +procps-ng|4.0.6-2|4.0.6-3|upgrade +python-cffi|2.0.0-2|2.1.0-1|upgrade +python-numpy|2.5.0-1|2.5.1-1|upgrade +python-pillow|12.2.0-1|12.3.0-1|upgrade +python-sentry_sdk|2.63.0-1|2.64.0-1|upgrade +python-setuptools|1:82.0.1-1|1:83.0.0-1|upgrade +python-typing_extensions|4.15.0-3|4.16.0-1|upgrade +qqc2-breeze-style|6.7.1-1|6.7.2-1|upgrade +qtkeychain-qt6|0.16.0-1|0.17.0-1|upgrade +rootlesskit|-|3.0.1-1|new +samba|2:4.24.3-1|2:4.24.4-1|upgrade +sddm-kcm|6.7.1-1|6.7.2-1|upgrade +sdl3|3.4.10-1|3.4.12-1|upgrade +shared-mime-info|2.4-3|2.5.1-1|upgrade +signon-kwallet-extension|26.04.2-1|26.04.3-1|upgrade +slirp4netns|-|1.3.4-1|new +smbclient|2:4.24.3-1|2:4.24.4-1|upgrade +spectacle|1:6.7.1-1|1:6.7.2-2|upgrade +systemsettings|6.7.1-1|6.7.2-1|upgrade +tmux|3.7-1|3.7_b-1|upgrade +tzdata|2026b-1|2026c-1|upgrade +upower|1.91.2-1|1.91.3-1|upgrade +vulkan-intel|1:26.1.3-2|1:26.1.4-1|upgrade +vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1|upgrade +vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1|upgrade +vulkan-radeon|1:26.1.3-2|1:26.1.4-1|upgrade +vulkan-swrast|1:26.1.3-2|1:26.1.4-1|upgrade +webkit2gtk-4.1|2.52.4-1|2.52.5-1|upgrade +webmin|2.610-1|2.651-1|upgrade +weechat|4.9.1-1|4.9.3-1|upgrade +xdg-desktop-portal-kde|6.7.1-1|6.7.2-1|upgrade +xorg-server-common|21.1.23-1|21.1.24-1|upgrade +xorg-server|21.1.23-1|21.1.24-1|upgrade +xorg-xrdb|1.2.2-2|1.2.3-1|upgrade +xorg-xsetroot|1.1.3-2|1.1.4-1|upgrade +xorg-xset|1.2.5-2|1.2.6-1|upgrade +xorg-xwayland|24.1.12-1|24.1.13-1|upgrade +xsettingsd|1.0.2-3|1.0.4-1|upgrade +yad|14.2-1|15.0-1|upgrade +zxing-cpp|3.0.2-1|3.1.0-1|upgrade diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/packaged-hooks.txt b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/packaged-hooks.txt new file mode 100644 index 0000000..b50c84c --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/packaged-hooks.txt @@ -0,0 +1,3 @@ +accountsservice-26.27.3-1-x86_64.pkg.tar.zst|usr/share/libalpm/hooks/accounts-daemon-restart.hook +openssh-10.4p1-2-x86_64.pkg.tar.zst|usr/share/libalpm/hooks/70-openssh-restart-sshd.hook +shared-mime-info-2.5.1-1-x86_64.pkg.tar.zst|usr/share/libalpm/hooks/30-update-mime-database.hook diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/scriptlets.txt b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/scriptlets.txt new file mode 100644 index 0000000..75ffdb4 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/scriptlets.txt @@ -0,0 +1,8 @@ +archlinux-keyring-20260707.1-1-any.pkg.tar.zst +gnupg-2.4.9-2-x86_64.pkg.tar.zst +gstreamer-1.28.5-1-x86_64.pkg.tar.zst +ksshaskpass-6.7.2-1-x86_64.pkg.tar.zst +samba-2:4.24.4-1-x86_64.pkg.tar.zst +shared-mime-info-2.5.1-1-x86_64.pkg.tar.zst +webmin-2.651-1-any.pkg.tar.zst +xorg-server-21.1.24-1-x86_64.pkg.tar.zst diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/totals.psv b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/totals.psv new file mode 100644 index 0000000..71f5b81 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/totals.psv @@ -0,0 +1,8 @@ +package_count|220 +upgrade_count|215 +new_count|5 +download_bytes|2082454915 +installed_delta_bytes|797229927 +backup_count|147 +scriptlet_count|8 +packaged_hook_count|3 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/upgrades.psv b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/upgrades.psv new file mode 100644 index 0000000..ba7b6d1 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/approved-transaction/upgrades.psv @@ -0,0 +1,215 @@ +tzdata|2026b-1|2026c-1 +libgcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libstdc++|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libevent|2.1.12-5|2.1.13-1 +libffi|3.6.0-1|3.7.1-1 +hwdata|0.408-1|0.409-1 +libssh2|1.11.1-5|1.11.1-6 +leancrypto|1.7.2-1|1.8.0-1 +accountsservice|26.26.9-1|26.27.3-1 +pinentry|1.3.2-2|1.3.3-1 +gnupg|2.4.9-1|2.4.9-2 +gpgme|2.1.1-1|2.1.2-1 +libgomp|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +archlinux-keyring|20260612-1|20260707.1-1 +libjpeg-turbo|3.1.4.1-1|3.2.0-2 +llvm-libs|22.1.6-1|22.1.8-1 +mesa|1:26.1.3-2|1:26.1.4-1 +liburing|2.14-1|2.15-1 +shared-mime-info|2.4-3|2.5.1-1 +libasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libhwasan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +liblsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libtsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libubsan|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libatomic|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libgfortran|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libobjc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libquadmath|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +libjxl|0.11.2-2|0.12.0-1 +libva|2.23.0-2|2.24.1-1 +libtiff|4.7.1-2|4.7.2-1 +bluez-libs|5.86-6|5.87-2 +glycin|2.1.5-1|2.1.5-2 +sdl3|3.4.10-1|3.4.12-1 +ffmpeg|2:8.1.2-7|2:8.1.2-10 +poppler|26.06.0-1|26.07.0-1 +poppler-qt6|26.06.0-1|26.07.0-1 +upower|1.91.2-1|1.91.3-1 +ark|26.04.2-1|26.04.3-1 +kdecoration|6.7.1-1|6.7.2-1 +aurorae|6.7.1-1|6.7.2-1 +baloo-widgets|26.04.2-2|26.04.3-1 +bash-completion|2.17.0-3|2.18.0-1 +plasma-activities|6.7.1-1|6.7.2-1 +libplasma|6.7.1-1|6.7.2-1 +bluez|5.86-6|5.87-2 +bluedevil|1:6.7.1-1|1:6.7.2-1 +bluez-hid2hci|5.86-6|5.87-2 +bluez-utils|5.86-6|5.87-2 +breeze-cursors|6.7.1-1|6.7.2-1 +breeze|6.7.1-1|6.7.2-1 +breeze-gtk|6.7.1-1|6.7.2-1 +c-ares|1.34.6-1|1.34.8-1 +cachyos-ananicy-rules-git|20260630.r2628.gebf4fa42-1|20260711.r2672.gc59d5971-1 +clamav|1.5.2-2|1.5.3-1 +libisl|0.27-1|0.28-1 +gcc|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +compiler-rt|22.1.6-1|22.1.8-1 +clang|22.1.6-1|22.1.8-1 +cmake|4.3.4-1|4.4.0-1 +signon-kwallet-extension|26.04.2-1|26.04.3-1 +noto-fonts|1:2026.06.01-1|1:2026.07.01-1 +kaccounts-integration|26.04.2-1|26.04.3-1 +discover|6.7.1-1|6.7.2-1 +docker-compose|5.1.4-1|5.3.1-1 +libkexiv2|26.04.2-1|26.04.3-1 +ldb|2:4.24.3-1|2:4.24.4-1 +libwbclient|2:4.24.3-1|2:4.24.4-1 +smbclient|2:4.24.3-1|2:4.24.4-1 +kio-extras|26.04.2-1|26.04.3-1 +dolphin|26.04.2-1|26.04.3-1 +python-sentry_sdk|2.63.0-1|2.64.0-1 +drkonqi|6.7.1-1|6.7.2-1 +electron42|42.3.0-1|42.6.1-1 +ethtool|1:7.0-1|1:7.1-1 +ostree|2025.7-3|2026.2-1 +gstreamer|1.28.4-2|1.28.5-1 +gst-plugins-base-libs|1.28.4-2|1.28.5-1 +flatpak-kcm|6.7.1-1|6.7.2-1 +freerdp|2:3.27.1-1|2:3.28.0-1 +fzf|0.73.1-1|0.74.0-1 +garuda-toolbox|5.3.2-1|5.3.2-2 +geoclue|2.8.1-1|2.8.2-1 +go|2:1.26.4-1|2:1.26.5-1 +gst-plugins-bad-libs|1.28.4-2|1.28.5-1 +gst-plugins-base|1.28.4-2|1.28.5-1 +libkdcraw|26.04.2-1|26.04.3-1 +gwenview|26.04.2-1|26.04.3-1 +imagemagick|7.1.2.26-1|7.1.2.26-2 +procps-ng|4.0.6-2|4.0.6-3 +inxi|3.3.40.1-1|3.3.41.1-1 +kactivitymanagerd|6.7.1-1|6.7.2-1 +kde-cli-tools|6.7.1-1|6.7.2-1 +xsettingsd|1.0.2-3|1.0.4-1 +kde-gtk-config|6.7.1-1|6.7.2-1 +kglobalacceld|6.7.1-1|6.7.2-1 +knighttime|6.7.1-1|6.7.2-1 +layer-shell-qt|6.7.1-1|6.7.2-1 +libkscreen|6.7.1-1|6.7.2-1 +kscreenlocker|6.7.1-1|6.7.2-1 +kwayland|6.7.1-1|6.7.2-1 +milou|6.7.1-1|6.7.2-1 +kwin|6.7.1-1|6.7.2-1 +kpipewire|6.7.1-1|6.7.2-1 +libksysguard|6.7.1-1|6.7.2-1 +ksystemstats|6.7.1-1|6.7.2-1 +ocean-sound-theme|6.7.1-1|6.7.2-1 +plasma-activities-stats|6.7.1-1|6.7.2-1 +xorg-xrdb|1.2.2-2|1.2.3-1 +libxfont2|2.0.7-1|2.0.8-1 +xorg-server-common|21.1.23-1|21.1.24-1 +xorg-xwayland|24.1.12-1|24.1.13-1 +qqc2-breeze-style|6.7.1-1|6.7.2-1 +xdg-desktop-portal-kde|6.7.1-1|6.7.2-1 +plasma-integration|6.7.1-1|6.7.2-1 +zxing-cpp|3.0.2-1|3.1.0-1 +plasma-workspace|6.7.1-1|6.7.2-1 +kdeplasma-addons|6.7.1-1|6.7.2-1 +kimageformats|6.27.0-1|6.27.0-2 +systemsettings|6.7.1-1|6.7.2-1 +kinfocenter|6.7.1-1|6.7.2-1 +kitty-terminfo|0.47.1-1|0.47.4-1 +kitty-shell-integration|0.47.1-1|0.47.4-1 +kitty|0.47.1-1|0.47.4-1 +kmenuedit|6.7.1-1|6.7.2-1 +konsole|26.04.2-1|26.04.3-1 +opencv|4.13.0-11|5.0.0-6 +kquickimageeditor|0.6.2-1|0.6.2.1-2 +qtkeychain-qt6|0.16.0-1|0.17.0-1 +krdp|6.7.1-1|6.7.2-1 +plasma5support|6.7.1-1|6.7.2-1 +kscreen|6.7.1-1|6.7.2-1 +ksshaskpass|6.7.1-1|6.7.2-1 +kwallet-pam|6.7.1-1|6.7.2-1 +kwrited|6.7.1-1|6.7.2-1 +lib32-gcc-libs|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +lib32-brotli|1.1.0-1|1.2.0-2 +lib32-libdatrie|0.2.13-3|0.2.14-1 +lib32-libffi|3.6.0-1|3.7.1-1 +lib32-libjpeg-turbo|3.1.4.1-1|3.2.0-1 +lib32-libssh2|1.11.1-3|1.11.1-4 +lib32-libthai|0.1.29-3|0.1.30-1 +lib32-libtiff|4.7.1-1|4.7.2-1 +lib32-llvm-libs|1:22.1.6-1|1:22.1.8-1 +lib32-mesa|1:26.1.3-2|1:26.1.4-1 +lib32-pam|1.7.1-1|1.7.2-1 +lib32-sdl3|3.4.10-1|3.4.12-1 +vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1 +lib32-vulkan-mesa-implicit-layers|1:26.1.3-2|1:26.1.4-1 +vulkan-intel|1:26.1.3-2|1:26.1.4-1 +lib32-vulkan-intel|1:26.1.3-2|1:26.1.4-1 +vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1 +lib32-vulkan-mesa-layers|1:26.1.3-2|1:26.1.4-1 +vulkan-radeon|1:26.1.3-2|1:26.1.4-1 +lib32-vulkan-radeon|1:26.1.3-2|1:26.1.4-1 +libgccjit|16.1.1+r346+g4e03491b401d-1|16.1.1+r346+g4e03491b401d-3 +librevenge|0.0.5-4|0.0.6-1 +libstaroffice|0.0.7-5|0.0.8-1 +libreoffice-fresh|26.2.4-3|26.2.4-4 +libxnvctrl|610.43.02-1|610.43.03-1 +linux|7.0.14.arch1-1|7.1.3.arch1-2 +linux-headers|7.0.14.arch1-1|7.1.3.arch1-2 +linux-zen|7.0.14.zen1-1|7.1.3.zen1-2 +linux-zen-headers|7.0.14.zen1-1|7.1.3.zen1-2 +llvm|22.1.6-1|22.1.8-1 +luajit|2.1.1780076327+b925b3e-1|2.1.1783773675+3c4f9fe-1 +mariadb-libs|12.3.2-2|12.3.2-3 +node-gyp|13.0.0-1|13.0.1-1 +npm|11.16.0-1|12.0.0-1 +nvidia-settings|610.43.02-1|610.43.03-1 +openssh|10.3p1-1|10.4p1-2 +oxygen-cursors|6.7.1-1|6.7.2-1 +oxygen|6.7.1-1|6.7.2-1 +oxygen-sounds|6.7.1-1|6.7.2-1 +perl-dbi|1.649-1|1.650-1 +perl-http-date|6.07-1|6.08-1 +php|8.5.7-1|8.5.8-1 +php-fpm|8.5.7-1|8.5.8-1 +php-gd|8.5.7-1|8.5.8-1 +php-pgsql|8.5.7-1|8.5.8-1 +php-sodium|8.5.7-1|8.5.8-1 +plasma-browser-integration|6.7.1-1|6.7.2-1 +polkit-kde-agent|6.7.1-1|6.7.2-1 +powerdevil|6.7.1-1|6.7.2-1 +plasma-desktop|6.7.1-1|6.7.2-1 +plasma-disks|6.7.1-1|6.7.2-1 +plasma-firewall|6.7.1-1|6.7.2-1 +plasma-keyboard|6.7.1-1|6.7.2-1 +plasma-login-manager|6.7.1-1|6.7.2-1 +plasma-nm|6.7.1-1|6.7.2-1 +plasma-pa|6.7.1-1|6.7.2-1 +plasma-systemmonitor|6.7.1-1|6.7.2-1 +plasma-thunderbolt|6.7.1-1|6.7.2-1 +plasma-vault|6.7.1-1|6.7.2-1 +plasma-welcome|6.7.1-1|6.7.2-1 +plasma-workspace-wallpapers|6.7.1-1|6.7.2-1 +print-manager|1:6.7.1-1|1:6.7.2-1 +python-cffi|2.0.0-2|2.1.0-1 +python-numpy|2.5.0-1|2.5.1-1 +python-pillow|12.2.0-1|12.3.0-1 +python-setuptools|1:82.0.1-1|1:83.0.0-1 +python-typing_extensions|4.15.0-3|4.16.0-1 +samba|2:4.24.3-1|2:4.24.4-1 +xorg-server|21.1.23-1|21.1.24-1 +sddm-kcm|6.7.1-1|6.7.2-1 +spectacle|1:6.7.1-1|1:6.7.2-2 +tmux|3.7-1|3.7_b-1 +vulkan-swrast|1:26.1.3-2|1:26.1.4-1 +webkit2gtk-4.1|2.52.4-1|2.52.5-1 +webmin|2.610-1|2.651-1 +weechat|4.9.1-1|4.9.3-1 +xorg-xset|1.2.5-2|1.2.6-1 +xorg-xsetroot|1.1.3-2|1.1.4-1 +yad|14.2-1|15.0-1 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh index 6e89195..624d2c4 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh @@ -10,7 +10,10 @@ fail() { manifest_value() { key=$1 file=$2 - value=$(awk -F= -v k="$key" '$1==k {sub(/^[^=]*=/, ""); print; found++} END {if(found!=1) exit 1}' "$file") + count=$(awk -F= -v k="$key" '$1==k {found++} END {print found+0}' "$file") + rc=$? + if [ "$rc" -ne 0 ] || [ "$count" != 1 ]; then return 1; fi + value=$(awk -F= -v k="$key" '$1==k {sub(/^[^=]*=/, ""); print}' "$file") rc=$? if [ "$rc" -ne 0 ]; then return "$rc"; fi printf '%s\n' "$value" @@ -89,11 +92,130 @@ validate_health_url_file() { return 0 } -rerun_volatile_checks() { - systemctl --failed --no-legend --plain --no-pager > "$record/volatile-failed-units.txt" 2>&1 +capture_failed_unit_fingerprint() { + output=$1 + : > "$output" + systemctl --failed --no-legend --plain --no-pager | awk '{print $1}' | LC_ALL=C sort | + while IFS= read -r unit; do + [ -n "$unit" ] || continue + printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ + "$(systemctl show "$unit" -p Id --value --no-pager)" \ + "$(systemctl show "$unit" -p Type --value --no-pager)" \ + "$(systemctl show "$unit" -p LoadState --value --no-pager)" \ + "$(systemctl show "$unit" -p ActiveState --value --no-pager)" \ + "$(systemctl show "$unit" -p SubState --value --no-pager)" \ + "$(systemctl show "$unit" -p Result --value --no-pager)" \ + "$(systemctl show "$unit" -p InvocationID --value --no-pager)" \ + "$(systemctl show "$unit" -p StateChangeTimestamp --value --no-pager)" \ + "$(systemctl show "$unit" -p FragmentPath --value --no-pager)" \ + "$(systemctl show "$unit" -p ExecMainCode --value --no-pager)" \ + "$(systemctl show "$unit" -p ExecMainStatus --value --no-pager)" + done > "$output" + return ${PIPESTATUS[0]} +} + +verify_failed_unit_disposition() { + output=$1 + (cd "$script_dir/failed-unit-disposition" && sha256sum -c SHA256SUMS) \ + > "$record/failed-unit-allowlist-checksum-latest.txt" 2>&1 rc=$? - [ "$rc" -eq 0 ] || { fail 'cannot capture current failed units' "$rc"; return $?; } - [ ! -s "$record/volatile-failed-units.txt" ] || { fail 'one or more failed units are present' 45; return $?; } + [ "$rc" -eq 0 ] || { fail 'failed-unit allowlist checksum failed' 45; return $?; } + capture_failed_unit_fingerprint "$output" + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot fingerprint current failed units' "$rc"; return $?; } + diff -u "$script_dir/failed-unit-disposition/drkonqi-failed-units.allowlist.psv" "$output" \ + > "$record/failed-unit-disposition-latest.diff" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'failed-unit set, state, invocation ID, or fingerprint changed' 45; return $?; } + count=$(wc -l < "$output") + [ "$count" = 234 ] || { fail "failed-unit count changed: expected=234 actual=$count" 45; return $?; } + return 0 +} + +verify_snapshot_subvolume() { + number=$1 + description=$2 + snapshot_dir=/.snapshots/$number + snapshot_subvolume=$snapshot_dir/snapshot + [ -d "$snapshot_dir" ] || { fail "Snapper numbered directory missing: $snapshot_dir" 50; return $?; } + btrfs subvolume show "$snapshot_subvolume" > "$record/pre-upgrade-snapshot-subvolume.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail "Snapper snapshot is not a verified Btrfs subvolume: $snapshot_subvolume" 50; return $?; } + snapper list --columns number,type,pre-number,date,user,cleanup,description \ + > "$record/snapper-snapshots-after-explicit-create.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot verify explicit snapshot through Snapper' "$rc"; return $?; } + awk -v number="$number" -v description="$description" \ + '$1 == number && index($0, description) {print}' \ + "$record/snapper-snapshots-after-explicit-create.txt" \ + > "$record/pre-upgrade-snapshot-list-match.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ] || [ ! -s "$record/pre-upgrade-snapshot-list-match.txt" ]; then + fail 'explicit snapshot number and description not found together in Snapper list' 50 + return $? + fi + return 0 +} + +create_explicit_pre_upgrade_snapshot() { + btrfs subvolume show /.snapshots > "$record/snapshots-subvolume-before-create.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail '/.snapshots is not a verified Btrfs subvolume' "$rc"; return $?; } + snapper list --columns number > "$record/snapper-numbers-before-explicit.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot capture Snapper number baseline' "$rc"; return $?; } + grep -E '^[[:space:]]*[0-9]+[[:space:]]*$' "$record/snapper-numbers-before-explicit.txt" | tr -d '[:space:]' | LC_ALL=C sort \ + > "$record/snapper-numbers-before-explicit.normalized" + description="le-phase2a-pre-upgrade-$(date -u +%Y%m%dT%H%M%SZ)-$(basename "$record")" + snapshot_output=$(snapper create --type single --cleanup-algorithm number \ + --description "$description" --print-number 2> "$record/pre-upgrade-snapshot-create.err") + rc=$? + printf '%s\n' "$snapshot_output" > "$record/pre-upgrade-snapshot-create.out" + [ "$rc" -eq 0 ] || { fail "explicit pre-upgrade Snapper snapshot failed rc=$rc" 50; return $?; } + snapshot_number=$(printf '%s' "$snapshot_output" | tr -d '[:space:]') + case "$snapshot_number" in ''|*[!0-9]*) fail 'Snapper returned an invalid snapshot number' 50; return $? ;; esac + [ "$snapshot_number" -gt 0 ] || { fail 'Snapper returned nonpositive snapshot number' 50; return $?; } + verify_snapshot_subvolume "$snapshot_number" "$description" || return $? + { + printf 'snapshot_number=%s\n' "$snapshot_number" + printf 'description=%s\n' "$description" + printf 'snapshot_directory=/.snapshots/%s\n' "$snapshot_number" + printf 'snapshot_subvolume=/.snapshots/%s/snapshot\n' "$snapshot_number" + } > "$record/pre-upgrade-snapshot.manifest" + btrfs qgroup show / > "$record/btrfs-quota-observation-before-upgrade.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + printf 'btrfs-quota-accounting|unavailable-or-disabled|rc=%s|explicit-snapshot-still-verified\n' "$rc" \ + >> "$record/warnings-before-upgrade.txt" + fi + return 0 +} + +verify_snap_pac_snapshots() { + snapper list --columns number > "$record/snapper-numbers-after-transaction.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot capture post-transaction Snapper numbers' "$rc"; return $?; } + grep -E '^[[:space:]]*[0-9]+[[:space:]]*$' "$record/snapper-numbers-after-transaction.txt" | tr -d '[:space:]' | LC_ALL=C sort \ + > "$record/snapper-numbers-after-transaction.normalized" + comm -13 "$record/snapper-numbers-before-explicit.normalized" "$record/snapper-numbers-after-transaction.normalized" \ + > "$record/snapper-new-numbers.txt" + new_count=$(wc -l < "$record/snapper-new-numbers.txt") + if [ "$new_count" -lt 3 ]; then + fail "expected explicit plus snap-pac pre/post snapshots; observed new snapshot count=$new_count" 51 + return $? + fi + while IFS= read -r number; do + case "$number" in ''|*[!0-9]*) fail 'invalid new Snapper snapshot number' 51; return $? ;; esac + btrfs subvolume show "/.snapshots/$number/snapshot" \ + > "$record/snapper-new-subvolume-$number.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail "new Snapper snapshot $number is not a Btrfs subvolume" 51; return $?; } + done < "$record/snapper-new-numbers.txt" + return 0 +} + +rerun_volatile_checks() { + verify_failed_unit_disposition "$record/volatile-failed-units-fingerprint.psv" || return $? pacman -Dk > "$record/volatile-pacman-Dk.txt" 2>&1 rc=$? [ "$rc" -eq 0 ] || { fail 'package database consistency failed immediately before execution' "$rc"; return $?; } @@ -161,6 +283,7 @@ main() { fi validate_preflight "$2" || return $? validate_health_url_file "$validated_preflight/health-urls.sanitized" || return $? + script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) umask 077 install -d -o root -g root -m 0700 /var/log/le-phase2a-maintenance @@ -179,15 +302,14 @@ main() { rc=$? [ "$rc" -eq 0 ] || { fail 'cannot copy validated preflight record' "$rc"; return $?; } - report=/tmp/le-phase2-package-decision-20260712.Mn7mIx/phase2-package-decision-report.md - report_manifest=/tmp/le-phase2-package-decision-20260712.Mn7mIx/SHA256SUMS.main - printf '%s %s\n' 195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d "$report" | sha256sum -c - \ + approved_transaction=$script_dir/approved-transaction + (cd "$approved_transaction" && sha256sum -c SHA256SUMS) \ > "$record/approved-report-checksum.txt" 2>&1 rc=$? - [ "$rc" -eq 0 ] || { fail 'approved report checksum failed' "$rc"; return $?; } - cp "$report" "$report_manifest" "$record/" + [ "$rc" -eq 0 ] || { fail 'approved transaction checksum failed' "$rc"; return $?; } + cp -a "$approved_transaction" "$record/approved-transaction" rc=$? - [ "$rc" -eq 0 ] || { fail 'cannot copy approved report and checksum manifest' "$rc"; return $?; } + [ "$rc" -eq 0 ] || { fail 'cannot copy approved transaction evidence' "$rc"; return $?; } uname -a > "$record/running-kernel-before.txt" 2>&1 rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture running kernel' "$rc"; return $?; } @@ -199,9 +321,9 @@ main() { "$validated_preflight/running-containers-before.txt" "$validated_preflight/containers-all.psv" \ "$validated_preflight/critical-units-baseline.psv" "$validated_preflight/database-baseline.psv" "$record/" rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot copy essential rollback baseline evidence' "$rc"; return $?; } - systemctl --failed --no-legend --plain --no-pager > "$record/failed-units-before.txt" 2>&1 - rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture failed-unit baseline' "$rc"; return $?; } - [ ! -s "$record/failed-units-before.txt" ] || { fail 'failed units appeared before transaction preview' 45; return $?; } + verify_failed_unit_disposition "$record/failed-units-before-fingerprint.psv" || return $? + btrfs subvolume show /.snapshots > "$record/snapshots-subvolume-before-preview.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail '/.snapshots is not a Btrfs subvolume' "$rc"; return $?; } rerun_volatile_checks || return $? @@ -228,9 +350,7 @@ main() { normalize_transaction "$record/latest-transaction.psv" "$record/latest-normalized-actions.psv" "$record/latest-downgrades.psv" [ ! -s "$record/latest-downgrades.psv" ] || { fail 'latest transaction contains a downgrade' 14; return $?; } - awk -F'|' '{print $1"|"$2"|"$3"|"$5}' \ - /tmp/le-phase2-package-decision-20260712.Mn7mIx/inventory.psv | sort \ - > "$record/approved-normalized-actions.psv" + cp "$approved_transaction/normalized-actions.psv" "$record/approved-normalized-actions.psv" rc=$? [ "$rc" -eq 0 ] || { fail 'cannot normalize approved preview' "$rc"; return $?; } diff -u "$record/approved-normalized-actions.psv" "$record/latest-normalized-actions.psv" \ @@ -273,11 +393,19 @@ main() { return $? fi + create_explicit_pre_upgrade_snapshot || return $? + printf 'At Pacman Y/n, answer no if any package, version, action, count, downgrade status, signature, or repository differs.\n' script -q -e -f -c 'pacman -Syu --needed rootlesskit slirp4netns openai-codex' "$record/pacman-complete.log" rc=$? if [ "$rc" -ne 0 ]; then fail "Pacman returned $rc; stop without ad hoc repair" "$rc"; return $?; fi + verify_snapshot_subvolume "$snapshot_number" "$description" || return $? + snapper list --columns number,type,pre-number,date,user,cleanup,description \ + > "$record/snapper-snapshots-after-transaction.txt" 2>&1 + rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot inventory Snapper snapshots after transaction' "$rc"; return $?; } + verify_snap_pac_snapshots || return $? + pacman -Q > "$record/packages-after.txt" 2>&1 rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture post-transaction package inventory' "$rc"; return $?; } diff -u "$record/packages-before.txt" "$record/packages-after.txt" > "$record/package-inventory.diff" 2>&1 @@ -308,6 +436,7 @@ main() { printf 'PACKAGE TRANSACTION COMPLETED; REBOOT NOT PERFORMED.\n' printf 'MAINTENANCE_RECORD=%s\n' "$record" + printf 'PRE_UPGRADE_SNAPPER_SNAPSHOT=%s\n' "$snapshot_number" printf 'Review all warnings, hooks, services, kernel artifacts, and pacnew/pacsave files.\n' printf 'Reboot and rootless-Docker provisioning remain separate approval gates.\n' return 0 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/README.md new file mode 100644 index 0000000..ec0edf4 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/README.md @@ -0,0 +1,15 @@ +# Reviewed failed-unit disposition + +The 234 entries in `drkonqi-failed-units.allowlist.psv` are transient +`drkonqi-coredump-processor@…` system services associated with desktop crash +processing. They are classified as nonproduction desktop crash-processing +failures, not production-critical or recovery-critical services. + +This is a fingerprinted maintenance-ticket exception, not a reset or repair. +Each row records unit identity, type, states, result, invocation ID, state-change +timestamp, fragment path, and main-process result. Preflight and immediate +pre-execution checks require exact byte equality. Any addition, removal, changed +invocation ID, state change, or checksum failure is a hard stop. No critical, +new, or otherwise undispositioned failed unit is admitted. + +Do not clear or reset these units merely to satisfy the maintenance gate. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/SHA256SUMS b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/SHA256SUMS new file mode 100644 index 0000000..98f2156 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/SHA256SUMS @@ -0,0 +1,2 @@ +e4667c83bd4e9e7de413e48cd16cc2d4b041cb09fbadace5e9f8d74a73a5a2a1 README.md +217a0fe59a3bff6c6621bcaac59dcad1c74154486cd7e75e3bba83b6909c29c4 drkonqi-failed-units.allowlist.psv diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/drkonqi-failed-units.allowlist.psv b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/drkonqi-failed-units.allowlist.psv new file mode 100644 index 0000000..d37d7e8 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/failed-unit-disposition/drkonqi-failed-units.allowlist.psv @@ -0,0 +1,234 @@ +drkonqi-coredump-processor@10943-42308-3031822_15595814-0.service|simple|loaded|failed|failed|timeout|8cad20db05aa4d489df8547cfb87eec8|Wed 2026-07-01 09:03:52 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@10948-30305-3032369_15595852-0.service|simple|loaded|failed|failed|timeout|6938a8ce71d24b9bbdc2b485235d5793|Wed 2026-07-01 09:03:55 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11360-1791-3102688_15629689-0.service|simple|loaded|failed|failed|timeout|f7b503d46373411d9d635dc9881f58a3|Wed 2026-07-01 09:09:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11361-9304-3102804_15641584-0.service|simple|loaded|failed|failed|timeout|e4d5a6df99c844c391c9851931552e74|Wed 2026-07-01 09:09:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11362-9305-3102803_15641583-0.service|simple|loaded|failed|failed|timeout|8d1c1281a49d487fbc99ac6f5a3ee468|Wed 2026-07-01 09:09:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11363-62442-3102805_15684272-0.service|simple|loaded|failed|failed|timeout|e922c447b8004bf58b16142fd40b751c|Wed 2026-07-01 09:09:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11364-47198-3102920_15641592-0.service|simple|loaded|failed|failed|timeout|59c5c21b78c14f19ab4f341f9986a16c|Wed 2026-07-01 09:09:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11367-59039-3104016_15688601-0.service|simple|loaded|failed|failed|timeout|671cedf59dab43f9be0dfe4790a23b36|Wed 2026-07-01 09:09:06 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11368-17567-3104018_15692076-0.service|simple|loaded|failed|failed|timeout|49eabc179bf04bffb0b30501a1c542e3|Wed 2026-07-01 09:09:06 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11369-17568-3104035_15692077-0.service|simple|loaded|failed|failed|timeout|e439e9bbc9fb47d5a1972b4442d7f320|Wed 2026-07-01 09:09:06 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11375-30355-3108455_15696030-0.service|simple|loaded|failed|failed|timeout|c80e355655ec4f95a12d93f75eaa5aab|Wed 2026-07-01 09:09:22 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11376-47200-3108748_15696071-0.service|simple|loaded|failed|failed|timeout|f30c6ea0bf0342e2bbc6c3639abb22eb|Wed 2026-07-01 09:09:22 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11378-17569-3108872_15681900-0.service|simple|loaded|failed|failed|timeout|6a83cad8025a4b1c988590d7f8cbd8af|Wed 2026-07-01 09:09:23 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11379-38193-3109212_15637535-0.service|simple|loaded|failed|failed|timeout|2bff76f468d443a2a7f69aa9a877c7fa|Wed 2026-07-01 09:09:23 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11381-47201-3109351_15684582-0.service|simple|loaded|failed|failed|timeout|e27e55525a534446a839ecb44e55da6e|Wed 2026-07-01 09:09:24 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11382-5350-3109701_15688962-0.service|simple|loaded|failed|failed|timeout|66a00d4bde5e4718b776143535157cf0|Wed 2026-07-01 09:09:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11384-26082-3109872_15666580-0.service|simple|loaded|failed|failed|timeout|0277d0913f7943a4bb7b9e21cf51dd6d|Wed 2026-07-01 09:09:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11385-26083-3110208_15689031-0.service|simple|loaded|failed|failed|timeout|91175e24e9f44a169dcb4d2530eaf1f2|Wed 2026-07-01 09:09:26 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11387-5351-3110405_15666592-0.service|simple|loaded|failed|failed|timeout|599d5562b18e4336946af4691f12ff0f|Wed 2026-07-01 09:09:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11389-13878-3110752_15666635-0.service|simple|loaded|failed|failed|timeout|7b24c609917a471a97da197935dc1810|Wed 2026-07-01 09:09:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11390-9306-3110908_15673649-0.service|simple|loaded|failed|failed|timeout|48d755ab407347878d4fee659353d215|Wed 2026-07-01 09:09:28 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11392-34352-3111207_15689092-0.service|simple|loaded|failed|failed|timeout|d766b1be42ba44b790b5916904921c65|Wed 2026-07-01 09:09:28 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11393-47204-3111339_15666702-0.service|simple|loaded|failed|failed|timeout|202ce0f14a2640a39b49ed2691bc05f3|Wed 2026-07-01 09:09:29 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11395-50509-3111709_15692622-0.service|simple|loaded|failed|failed|timeout|83b93fcce818403a9a903a841eb2f2c2|Wed 2026-07-01 09:09:30 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11396-54216-3111859_15666742-0.service|simple|loaded|failed|failed|timeout|64e7c13520b540198a22db2b947a71cc|Wed 2026-07-01 09:09:30 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11398-5354-3112183_15624237-0.service|simple|loaded|failed|failed|timeout|fbae4a151275441ca6f80e69eac3e53f|Wed 2026-07-01 09:09:31 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11399-9307-3112343_15666807-0.service|simple|loaded|failed|failed|timeout|dad67de8fe454533bb8756db6f01a905|Wed 2026-07-01 09:09:32 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11400-34353-3112631_15673794-0.service|simple|loaded|failed|failed|timeout|7d5189e667d842ecad29447874f43d13|Wed 2026-07-01 09:09:32 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11401-50510-3112655_15670319-0.service|simple|loaded|failed|failed|timeout|6eae425e9ebb48cea55f058f3cefd1bd|Wed 2026-07-01 09:09:32 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11403-21557-3113110_15642065-0.service|simple|loaded|failed|failed|timeout|93d0eea3e69e42d68d9f0b9b8a2f25d2|Wed 2026-07-01 09:09:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11404-38194-3113120_15642070-0.service|simple|loaded|failed|failed|timeout|2454f0f4a6e64cd9b76d65dffe0b2df3|Wed 2026-07-01 09:09:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11405-59040-3113299_15682325-0.service|simple|loaded|failed|failed|timeout|d2cfdbae0a23498b8b923132f9583192|Wed 2026-07-01 09:09:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11406-30358-3113602_15682350-0.service|simple|loaded|failed|failed|timeout|544bdb4583d94a64a2201f0d42f6dac3|Wed 2026-07-01 09:09:35 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11407-50511-3113620_15684848-0.service|simple|loaded|failed|failed|timeout|c51fe9e517114151b49ae3d6adef2637|Wed 2026-07-01 09:09:35 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11408-34354-3113830_15666922-0.service|simple|loaded|failed|failed|timeout|57407a552e6f45d5b715f1a832bb19dd|Wed 2026-07-01 09:09:35 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11409-17570-3114129_15704112-0.service|simple|loaded|failed|failed|timeout|950ffe0370434e46996e2a282e69529c|Wed 2026-07-01 09:09:36 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11411-50512-3114195_15696352-0.service|simple|loaded|failed|failed|timeout|39689d155efe4a78b06e8aac0deb10db|Wed 2026-07-01 09:09:36 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11412-47205-3115453_15637889-0.service|simple|loaded|failed|failed|timeout|31bcf4a6b4e545829047e365eae014bf|Wed 2026-07-01 09:09:37 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11413-13879-3115596_15630186-0.service|simple|loaded|failed|failed|timeout|3ecdd88c9a8b4f0282657cf3b2f38fcb|Wed 2026-07-01 09:09:37 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11416-59042-3116108_15673992-0.service|simple|loaded|failed|failed|timeout|184d1f726a6346e1b008412558e06009|Wed 2026-07-01 09:09:38 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11417-1793-3116115_15637922-0.service|simple|loaded|failed|failed|timeout|de58c21c9e7a496193441e2d3acc637c|Wed 2026-07-01 09:09:38 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11418-50513-3116306_15685236-0.service|simple|loaded|failed|failed|timeout|a19ecf6379774f49a46780f7f943ac78|Wed 2026-07-01 09:09:39 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11419-34356-3116618_15689417-0.service|simple|loaded|failed|failed|timeout|dfba10ad3c564337b5bdafeb0d956cf5|Wed 2026-07-01 09:09:39 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11420-26085-3116643_15637963-0.service|simple|loaded|failed|failed|timeout|d3fcd22ffffd44b69d20c0a95c4f1135|Wed 2026-07-01 09:09:39 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11421-47206-3116759_15674033-0.service|simple|loaded|failed|failed|timeout|fb0fb34d9d2141c8856cb7908a522744|Wed 2026-07-01 09:09:40 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11423-34358-3117082_15682548-0.service|simple|loaded|failed|failed|timeout|a6f489aca6da44828c8d14b717e1ed89|Wed 2026-07-01 09:09:40 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11424-59043-3117226_15685287-0.service|simple|loaded|failed|failed|timeout|7da9e691ba7a46dd9e94500a922a5c32|Wed 2026-07-01 09:09:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11427-42374-3117621_15682609-0.service|simple|loaded|failed|failed|timeout|4b599161467f488da6ede44e72b97293|Wed 2026-07-01 09:09:42 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11429-38195-3117971_15649979-0.service|simple|loaded|failed|failed|timeout|db5f3317cb8e47cfaf1b95231d207838|Wed 2026-07-01 09:09:43 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11430-21558-3118094_15649988-0.service|simple|loaded|failed|failed|timeout|c9a3820da60a4ad6bb71a50fc10b46d6|Wed 2026-07-01 09:09:43 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11431-47207-3118428_15638032-0.service|simple|loaded|failed|failed|timeout|bfc74754fc054c6ab713eb3c499209a0|Wed 2026-07-01 09:09:44 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11433-9308-3118549_15685370-0.service|simple|loaded|failed|failed|timeout|78af9bd4a5054cf8a4a0ceca5037d657|Wed 2026-07-01 09:09:44 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11434-1795-3118841_15650022-0.service|simple|loaded|failed|failed|timeout|a458790b575f456480d87bd2abfbcace|Wed 2026-07-01 09:09:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11435-5355-3118862_15650025-0.service|simple|loaded|failed|failed|timeout|c507968071ec4ee5b1067a8dab72fea1|Wed 2026-07-01 09:09:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11436-30359-3118990_15685387-0.service|simple|loaded|failed|failed|timeout|016f5d439fca4f099cd43ca2c4259838|Wed 2026-07-01 09:09:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11438-17572-3119336_15700642-0.service|simple|loaded|failed|failed|timeout|806867f022fc49ef971f67f27b7929be|Wed 2026-07-01 09:09:46 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11439-26086-3119450_15708253-0.service|simple|loaded|failed|failed|timeout|1945f4076cfa4e63abeeb12f861b8555|Wed 2026-07-01 09:09:46 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11440-30360-3119787_15696700-0.service|simple|loaded|failed|failed|timeout|f8607241dacd41cbaa46bed3af330925|Wed 2026-07-01 09:09:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11441-30361-3119800_15693182-0.service|simple|loaded|failed|failed|timeout|7a3119d590344027bb5952c8c92d11cc|Wed 2026-07-01 09:09:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11442-50514-3119886_15661010-0.service|simple|loaded|failed|failed|timeout|b5b3cfdeb6f741f3b575321eb560b009|Wed 2026-07-01 09:09:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11443-50515-3120204_15638134-0.service|simple|loaded|failed|failed|timeout|c07d4a47146a45f3ab40a28fff87ef0d|Wed 2026-07-01 09:09:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11444-26087-3120210_15712385-0.service|simple|loaded|failed|failed|timeout|cd9337cd22a24b648073d2c09c1664cc|Wed 2026-07-01 09:09:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11446-17573-3120602_15624422-0.service|simple|loaded|failed|failed|timeout|afd343a3d30a41d0861b93c036ef9954|Wed 2026-07-01 09:09:49 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11448-59046-3120696_15650137-0.service|simple|loaded|failed|failed|timeout|dd701b827b37452d9c2403cbb6a15604|Wed 2026-07-01 09:09:49 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11450-1797-3120977_15682861-0.service|simple|loaded|failed|failed|timeout|61e04d9454844a4b810e6b90977e92e5|Wed 2026-07-01 09:09:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11451-13880-3121104_15704559-0.service|simple|loaded|failed|failed|timeout|94d720f7abdc411f9e8ddd4b7a495ee2|Wed 2026-07-01 09:09:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11452-26088-3121382_15670849-0.service|simple|loaded|failed|failed|timeout|f0ddd2f525d645e8933671491d0769dd|Wed 2026-07-01 09:09:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11453-9309-3121555_15638180-0.service|simple|loaded|failed|failed|timeout|0e6ac840361549468508ba08bbc3c053|Wed 2026-07-01 09:09:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11454-9310-3121556_15638181-0.service|simple|loaded|failed|failed|timeout|9a7dd1a29e284c28af95971295cf1063|Wed 2026-07-01 09:09:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11455-13881-3121657_15696811-0.service|simple|loaded|failed|failed|timeout|19fb737275004a999251d67950d16420|Wed 2026-07-01 09:09:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11456-38196-3121886_15624438-0.service|simple|loaded|failed|failed|timeout|cb4ebf309816494483599094764a27a3|Wed 2026-07-01 09:09:52 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11457-5357-3121974_15700779-0.service|simple|loaded|failed|failed|timeout|0c4418fd880f49fb919c51e631424c17|Wed 2026-07-01 09:09:52 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11458-13882-3122168_15682978-0.service|simple|loaded|failed|failed|timeout|65f49cc58c7341318637e50d0dfb5f81|Wed 2026-07-01 09:09:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11460-47209-3122345_15674436-0.service|simple|loaded|failed|failed|timeout|40047c7f47cf41beaf80511ad32f65fe|Wed 2026-07-01 09:09:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11463-5359-3123156_15650282-0.service|simple|loaded|failed|failed|timeout|489da5918b3d4cd79bca358ee4b236ca|Wed 2026-07-01 09:09:56 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11464-21559-3123233_15683051-0.service|simple|loaded|failed|failed|timeout|23ae52d1b2004606a62aa9a320b8392b|Wed 2026-07-01 09:09:56 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11465-13883-3124816_15671054-0.service|simple|loaded|failed|failed|timeout|c3c01e64e2084e86b882e8a812693ce7|Wed 2026-07-01 09:09:59 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11466-13884-3124849_15693616-0.service|simple|loaded|failed|failed|timeout|5f6e60ede55c4ff196a6d4b2c1ba5490|Wed 2026-07-01 09:10:00 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11467-54217-3124853_15674529-0.service|simple|loaded|failed|failed|timeout|d730fcb976ee4d4db620c5afdbb65a40|Wed 2026-07-01 09:10:00 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11468-54218-3124857_15624485-0.service|simple|loaded|failed|failed|timeout|db38eda248404806947ed4f1ea4ad016|Wed 2026-07-01 09:10:00 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@11570-62464-3146774_15702033-0.service|simple|loaded|failed|failed|timeout|66d6667540da4014998647e3dbcace87|Wed 2026-07-01 09:12:39 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@12466-38290-3187943_15723747-0.service|simple|loaded|failed|failed|timeout|dae0a26204b54b7bb2b8974f2c1a96c9|Wed 2026-07-01 09:16:20 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13-24579-4156318_12550645-0.service|simple|loaded|failed|failed|timeout|3d160086565c4082b7a88b24ed99302d|Wed 2026-07-01 05:45:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13134-62781-3219031_15740709-0.service|simple|loaded|failed|failed|timeout|daded650cec84d9fb89781f4da75ae29|Wed 2026-07-01 09:19:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@131596-276963-4118490_46027792-0.service|simple|loaded|failed|failed|exit-code|4bc41d8d21cf4faeaba25a6c4619a6d9|Sat 2026-07-04 15:50:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|1|1 +drkonqi-coredump-processor@131598-208455-2258635_69245845-0.service|simple|loaded|failed|failed|exit-code|59d4c4a7c43e42268a793f4af65e3819|Tue 2026-07-07 08:08:17 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|1|1 +drkonqi-coredump-processor@13236-59212-3223330_15763239-0.service|simple|loaded|failed|failed|timeout|353320a4c6d841568a7fd02b06c59a52|Wed 2026-07-01 09:19:26 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13296-34565-3226264_15811266-0.service|simple|loaded|failed|failed|timeout|dce645f59fa44302aec8f4896f79084b|Wed 2026-07-01 09:19:38 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13431-17779-3231347_15807323-0.service|simple|loaded|failed|failed|timeout|dabedf5bc4b64e6fba1544ac36d8b764|Wed 2026-07-01 09:20:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13459-26210-3232389_15814944-0.service|simple|loaded|failed|failed|timeout|ad6800e75f51413b9ae914429796b4e3|Wed 2026-07-01 09:20:10 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13464-9594-3232504_15819395-0.service|simple|loaded|failed|failed|timeout|67d5035f25e64b2390925582fdd6bd03|Wed 2026-07-01 09:20:11 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13524-21902-3235148_15777090-0.service|simple|loaded|failed|failed|timeout|edae94a9895f4b959356c1528bb70258|Wed 2026-07-01 09:20:22 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13544-21905-3235696_15781694-0.service|simple|loaded|failed|failed|timeout|6c4a83d33c7d43bdb72acb60607e0747|Wed 2026-07-01 09:20:26 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13598-62877-3238444_15800050-0.service|simple|loaded|failed|failed|timeout|58eab954c6f14ab793640a1c2a41b507|Wed 2026-07-01 09:20:37 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13676-59241-3240761_15823440-0.service|simple|loaded|failed|failed|timeout|3fe1e18fc7e9459d8644c35c6706a58d|Wed 2026-07-01 09:20:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13740-21930-3243742_15812073-0.service|simple|loaded|failed|failed|timeout|508e93ad584e4dc3a15d9f0e27f830f9|Wed 2026-07-01 09:21:05 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13814-62926-3247059_15828217-0.service|simple|loaded|failed|failed|timeout|b440e45850934c1a9a896aeaed7d82e7|Wed 2026-07-01 09:21:20 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13870-30738-3248680_15847525-0.service|simple|loaded|failed|failed|timeout|ae51a243469e41208ecb8a7f5883a2c1|Wed 2026-07-01 09:21:31 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13880-38402-3248944_15835666-0.service|simple|loaded|failed|failed|timeout|426b7d46ff02437782c7d1e7c4ad7889|Wed 2026-07-01 09:21:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@13881-38403-3248945_15835667-0.service|simple|loaded|failed|failed|timeout|15dd786dbe5541ffa57aaf8a24aa5889|Wed 2026-07-01 09:21:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14069-54991-3257157_15798139-0.service|simple|loaded|failed|failed|timeout|80aef7efcf6241728035b03817abbcff|Wed 2026-07-01 09:22:13 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14139-17855-3260131_15848059-0.service|simple|loaded|failed|failed|timeout|bed84620cc364de491f61cb5da56518f|Wed 2026-07-01 09:22:26 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14148-34680-3260320_15808700-0.service|simple|loaded|failed|failed|timeout|4fe19dd4a0304d9eae0a9247cad6b662|Wed 2026-07-01 09:22:28 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14240-38435-3264419_15848267-0.service|simple|loaded|failed|failed|timeout|1f52dba8207e42619bcd8063f1c6e105|Wed 2026-07-01 09:22:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@1433-49360-236163_12782713-0.service|simple|loaded|failed|failed|timeout|f053cd8a32d84bc28e890d4ccb0e88cf|Wed 2026-07-01 06:03:24 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14348-55072-3268928_15856372-0.service|simple|loaded|failed|failed|timeout|e948136d14494b3f8886a4a310971a80|Wed 2026-07-01 09:23:10 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14370-9732-3269638_15856403-0.service|simple|loaded|failed|failed|timeout|facde2eefacb4b80827b9fcb8fee12d5|Wed 2026-07-01 09:23:15 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14382-2110-3270943_15825122-0.service|simple|loaded|failed|failed|timeout|b2b4fcbf5cf849c9a4e01f1ec8ee201d|Wed 2026-07-01 09:23:17 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14513-9756-3275959_15765493-0.service|simple|loaded|failed|failed|timeout|01e218f4b81941bf811ff373b485d98d|Wed 2026-07-01 09:23:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14521-63057-3276214_15844775-0.service|simple|loaded|failed|failed|timeout|2cfb310db94a4c5795b50aa1b741de6a|Wed 2026-07-01 09:23:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14559-30826-3277265_15867988-0.service|simple|loaded|failed|failed|timeout|3bea4e89812744bc8b68a802baf17d62|Wed 2026-07-01 09:23:55 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14600-59324-3279522_15860161-0.service|simple|loaded|failed|failed|timeout|ca2bb774164b4d9f9d992ee7e11a8abd|Wed 2026-07-01 09:24:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14623-50718-3280432_15849141-0.service|simple|loaded|failed|failed|timeout|09a9bdec78b741c3a9a4927aa041f1b1|Wed 2026-07-01 09:24:10 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14646-42586-3281123_15829990-0.service|simple|loaded|failed|failed|timeout|b9c1fb4f60cf4d0fb39f5ebbfb1966f6|Wed 2026-07-01 09:24:15 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14650-38477-3281211_15801882-0.service|simple|loaded|failed|failed|timeout|3aabe759e34f49bd99e41ab668235e31|Wed 2026-07-01 09:24:15 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14666-2144-3282610_15864319-0.service|simple|loaded|failed|failed|timeout|5415155d6fbc42f7bd957f1eb73e2e6b|Wed 2026-07-01 09:24:19 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14692-55138-3283254_15849356-0.service|simple|loaded|failed|failed|timeout|9c1cd16feb87463aa0ec01ea761c1188|Wed 2026-07-01 09:24:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14730-63079-3284428_15860390-0.service|simple|loaded|failed|failed|timeout|6dc71af834a245ee8d4e4da81ac632fe|Wed 2026-07-01 09:24:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14771-2155-3286754_15880268-0.service|simple|loaded|failed|failed|timeout|a3d86e8bfefb4d88aac179d22e3964b1|Wed 2026-07-01 09:24:43 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14781-34747-3287031_15833622-0.service|simple|loaded|failed|failed|timeout|a1a3558f847d4785afa138b0feae2ca6|Wed 2026-07-01 09:24:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14871-17953-3290505_15864977-0.service|simple|loaded|failed|failed|timeout|de585e804a0b43b4bd8b596338e59d49|Wed 2026-07-01 09:25:04 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@14896-63126-3291459_15857640-0.service|simple|loaded|failed|failed|timeout|b4ee6494526e4e50ba2b866d1b0a9e29|Wed 2026-07-01 09:25:10 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@15006-55200-3295302_15842036-0.service|simple|loaded|failed|failed|timeout|25d1c1a6c5ad4758bb0212304e421e6e|Wed 2026-07-01 09:25:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@15332-63219-3309221_15818237-0.service|simple|loaded|failed|failed|timeout|30a1585f309742888b41f2b16bf6d148|Wed 2026-07-01 09:26:52 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@15533-47702-3318533_15905544-0.service|simple|loaded|failed|failed|timeout|b9cff1e1d0f5474eb65092c97377a013|Wed 2026-07-01 09:27:42 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@16010-50798-3339357_15910047-0.service|simple|loaded|failed|failed|timeout|d8749db2511a4796aed098ce622e7f85|Wed 2026-07-01 09:29:40 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@16406-2322-3357040_15863535-0.service|simple|loaded|failed|failed|timeout|882b215d3fc34f779480767b484d34b6|Wed 2026-07-01 09:31:20 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@16716-63519-3369869_15950355-0.service|simple|loaded|failed|failed|timeout|e14522394fa14bf69bdbd97aeca49c13|Wed 2026-07-01 09:32:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@1694-57589-305530_12883699-0.service|simple|loaded|failed|failed|timeout|6d66be104fb441a6a6563f7e6307d234|Wed 2026-07-01 06:06:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17021-63595-3383609_15932538-0.service|simple|loaded|failed|failed|timeout|35bae0d5c5934db5aa20d1b8f145ec29|Wed 2026-07-01 09:33:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17026-18171-3383769_15967486-0.service|simple|loaded|failed|failed|timeout|b5e6f259595449a7a115bc86b98dfafb|Wed 2026-07-01 09:33:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17099-10133-3387258_15887742-0.service|simple|loaded|failed|failed|timeout|612be4d7d3b647468c8ecf97dbdf7409|Wed 2026-07-01 09:34:07 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17225-22555-3393373_15933102-0.service|simple|loaded|failed|failed|timeout|a52dbc20c2af4a6a9d834caf57982082|Wed 2026-07-01 09:34:39 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17263-10168-3394602_15963418-0.service|simple|loaded|failed|failed|timeout|25469cd45b8c41b6a1078415f481ae5e|Wed 2026-07-01 09:34:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17270-38771-3394828_15978657-0.service|simple|loaded|failed|failed|timeout|d3d9b3b62f5646128eff31b757457c13|Wed 2026-07-01 09:34:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17421-10197-3401350_15949433-0.service|simple|loaded|failed|failed|timeout|20c8c6a86db5450e81e1c416b6596edd|Wed 2026-07-01 09:35:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@17889-10286-3421021_15988409-0.service|simple|loaded|failed|failed|timeout|7c0a7d0fa61d4e6ca470f5ad9d034618|Wed 2026-07-01 09:37:22 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@18126-59636-3429717_16008653-0.service|simple|loaded|failed|failed|timeout|08a9dae1ce744d00b4e102475064818f|Wed 2026-07-01 09:38:20 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@1813-24862-322240_12888243-0.service|simple|loaded|failed|failed|timeout|0961358f2841439d8ed512db4bb31984|Wed 2026-07-01 06:08:13 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@18499-56168-3442605_15990193-0.service|simple|loaded|failed|failed|timeout|f175e2aecafc437b9beda31a3f926987|Wed 2026-07-01 09:39:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@19074-59720-3460907_16021541-0.service|simple|loaded|failed|failed|timeout|a2a299fcb59842819ded22d0cfe72584|Wed 2026-07-01 09:42:09 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@19131-31623-3462680_15997803-0.service|simple|loaded|failed|failed|timeout|63c25c6ea18c4bdda6eb4546d9debd4c|Wed 2026-07-01 09:42:23 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@19500-35213-3474686_16041532-0.service|simple|loaded|failed|failed|timeout|baf958254c014e438ca30f761d19cbff|Wed 2026-07-01 09:43:55 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@19936-42871-3488796_16030802-0.service|simple|loaded|failed|failed|timeout|6c5565b3d3294b69a1f2fc1eae569627|Wed 2026-07-01 09:45:42 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@20234-26675-3498226_16066751-0.service|simple|loaded|failed|failed|timeout|76c2ccf157a44a5793ce3d264d6ed695|Wed 2026-07-01 09:46:57 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@20319-23165-3501109_16077553-0.service|simple|loaded|failed|failed|timeout|b13aae259e9944d89f521f00a291dac7|Wed 2026-07-01 09:47:19 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@20405-64411-3503833_16034889-0.service|simple|loaded|failed|failed|timeout|8aa625e38a694d19adfb9da1855d653c|Wed 2026-07-01 09:47:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@20779-56825-3515776_16090219-0.service|simple|loaded|failed|failed|timeout|5930255ba53e4c07b776e10c673d945f|Wed 2026-07-01 09:49:12 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2119-24902-382403_12925739-0.service|simple|loaded|failed|failed|timeout|7593b18300d74fe4af84f4eb287088a7|Wed 2026-07-01 06:11:57 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@21536-57071-3539933_16122729-0.service|simple|loaded|failed|failed|timeout|1caf83ed90154623a40cb3c4e59a0a0e|Wed 2026-07-01 09:52:20 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@21867-2813-3550415_16107233-0.service|simple|loaded|failed|failed|timeout|fe9aaa89bcba455da2b4594e7f6e1baa|Wed 2026-07-01 09:53:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@22297-23532-3563962_16076366-0.service|simple|loaded|failed|failed|timeout|1a22522d3d174699ac98950820ddab26|Wed 2026-07-01 09:55:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2242-33034-402341_12981452-0.service|simple|loaded|failed|failed|timeout|7bf44071477c4ab1ac39073d71375589|Wed 2026-07-01 06:13:29 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2281-20666-407393_12926804-0.service|simple|loaded|failed|failed|timeout|d418c22968c34faaa15c0af41532d398|Wed 2026-07-01 06:13:57 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@22822-26813-3581672_16175362-0.service|simple|loaded|failed|failed|timeout|a40b752c512644c19f9d72bf5d17f264|Wed 2026-07-01 09:57:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@22840-32262-3582169_16147403-0.service|simple|loaded|failed|failed|timeout|5bab26e3e2ec4ace9603c9f3d7745519|Wed 2026-07-01 09:57:52 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@23326-60098-3597985_16164500-0.service|simple|loaded|failed|failed|timeout|d3b34eddfbe84feaa92d6ccbb0ed8983|Wed 2026-07-01 09:59:57 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2345-33060-415734_12971094-0.service|simple|loaded|failed|failed|timeout|c3b6d00b31f143138e603d29018fa842|Wed 2026-07-01 06:14:42 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2746-41355-473046_13020830-0.service|simple|loaded|failed|failed|timeout|09e801a17d6a470bbb7e740c3bb0b768|Wed 2026-07-01 06:19:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@2770-49526-476322_13048264-0.service|simple|loaded|failed|failed|timeout|f5b26a01b45b4727b3a5783545d6f97d|Wed 2026-07-01 06:19:51 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3154-12777-529354_13097002-0.service|simple|loaded|failed|failed|timeout|dfd791065c1f45368e49f699b50fc946|Wed 2026-07-01 06:24:30 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3163-49556-530454_13088294-0.service|simple|loaded|failed|failed|timeout|c1a34262131e4e6783c06d7823a8fd90|Wed 2026-07-01 06:24:36 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3186-12792-533428_13093732-0.service|simple|loaded|failed|failed|timeout|dcafdd5f049f440bbacbd3be05e9846a|Wed 2026-07-01 06:24:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3187-25086-533485_13097438-0.service|simple|loaded|failed|failed|timeout|390610044a654302b59bb8f39d39f940|Wed 2026-07-01 06:24:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3465-29174-570570_13153181-0.service|simple|loaded|failed|failed|timeout|d83ebabb95bd420abe6739bf56ec37d8|Wed 2026-07-01 06:28:15 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3466-29175-570626_13134401-0.service|simple|loaded|failed|failed|timeout|76c11764e46a4adc8c819ee8584e37de|Wed 2026-07-01 06:28:15 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@3657-45733-598696_13181698-0.service|simple|loaded|failed|failed|timeout|d4a767c7d3374f3485257b7ad7d22f5f|Wed 2026-07-01 06:30:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4262-770-685798_13256821-0.service|simple|loaded|failed|failed|timeout|1cb4cde1dc1b44d18f8450739a2d9b5a|Wed 2026-07-01 06:37:58 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4289-61745-692733_13221409-0.service|simple|loaded|failed|failed|timeout|23da818c4a114726951aba055b3c1769|Wed 2026-07-01 06:38:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4290-61746-692741_13244643-0.service|simple|loaded|failed|failed|timeout|38ab185832d14ea395789bdc722bba1a|Wed 2026-07-01 06:38:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4291-61747-692745_13221410-0.service|simple|loaded|failed|failed|timeout|14b2443b2dce499e8bf6ab49983769be|Wed 2026-07-01 06:38:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4292-61748-692748_13221411-0.service|simple|loaded|failed|failed|timeout|4d171be7814b422a88d9924e587283a3|Wed 2026-07-01 06:38:25 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4294-53527-695192_13216861-0.service|simple|loaded|failed|failed|timeout|2c0ec981b46b4e4a82b7783177d5118a|Wed 2026-07-01 06:38:30 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@4393-791-1106971_13689616-0.service|simple|loaded|failed|failed|timeout|86ba870182964cd1aefd831b71504e4a|Wed 2026-07-01 06:56:56 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@466-8238-44588_12640654-0.service|simple|loaded|failed|failed|timeout|fe7d733f35ae48a6bfdc2426458f1c93|Wed 2026-07-01 05:51:05 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@523-20519-52453_12617501-0.service|simple|loaded|failed|failed|timeout|6e44682ce6014c0cb84dbe5a1d934d38|Wed 2026-07-01 05:51:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@5488-16973-1385714_13960660-0.service|simple|loaded|failed|failed|timeout|8b5706a6698744dabea24ec9f0abdce9|Wed 2026-07-01 07:10:38 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@5540-33567-1395820_13952919-0.service|simple|loaded|failed|failed|timeout|662cb311c87540539b501fe433ee90c9|Wed 2026-07-01 07:11:16 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@5842-41723-1466885_14042085-0.service|simple|loaded|failed|failed|timeout|e86414c74a8e43e680f20d483d29f255|Wed 2026-07-01 07:15:01 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@5843-41724-1466893_14042087-0.service|simple|loaded|failed|failed|timeout|ead617a519234e3caeddec7f0473874a|Wed 2026-07-01 07:15:01 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@5844-46229-1466899_14042088-0.service|simple|loaded|failed|failed|timeout|088c16ee14f5477693c75f2b26de7ca9|Wed 2026-07-01 07:15:01 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@689-41044-75904_12661204-0.service|simple|loaded|failed|failed|timeout|ab6623bfc87f44d2be3bd83fcb703dd5|Wed 2026-07-01 05:53:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6942-49991-1692057_14225101-0.service|simple|loaded|failed|failed|timeout|5831a6344fdd4944938b10d5e4e65940|Wed 2026-07-01 07:28:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6943-13351-1692055_14286940-0.service|simple|loaded|failed|failed|timeout|26f51730bb2b4230a4bedefe7aaa4b0e|Wed 2026-07-01 07:28:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6944-13352-1692058_14225102-0.service|simple|loaded|failed|failed|timeout|cca66f31d6324570a72a7e5a8ff33e80|Wed 2026-07-01 07:28:33 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6946-46461-1692549_14275440-0.service|simple|loaded|failed|failed|timeout|4ededacc8e454772aa3af417ea52c19d|Wed 2026-07-01 07:28:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6948-8767-1692598_14225126-0.service|simple|loaded|failed|failed|timeout|4dd75d58a0ca41fcbf52b0716880077b|Wed 2026-07-01 07:28:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6950-20999-1692668_14267380-0.service|simple|loaded|failed|failed|timeout|2b6351f772d74e4188804e64df9e3ebb|Wed 2026-07-01 07:28:35 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6954-1199-1695030_14287069-0.service|simple|loaded|failed|failed|timeout|01298b25741943119f62928da98c5fa2|Wed 2026-07-01 07:28:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6957-46462-1696774_14260107-0.service|simple|loaded|failed|failed|timeout|e6211873748e4b189c367837adf66570|Wed 2026-07-01 07:28:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6959-8769-1697314_14280216-0.service|simple|loaded|failed|failed|timeout|2001941a567d4ea0bd9eeaf5894d1c81|Wed 2026-07-01 07:28:53 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6960-4857-1698429_14200068-0.service|simple|loaded|failed|failed|timeout|79cc405a471843c3a2b414c74c05cf16|Wed 2026-07-01 07:28:58 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6962-21001-1699086_14233301-0.service|simple|loaded|failed|failed|timeout|3272868b57314b0481b9fba9f35e808d|Wed 2026-07-01 07:29:01 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6963-21002-1699099_14291180-0.service|simple|loaded|failed|failed|timeout|0946f4352a5041bab51b8d4c4b01f6d0|Wed 2026-07-01 07:29:01 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6968-53719-1701486_14271032-0.service|simple|loaded|failed|failed|timeout|ae13f925ae544b229af651368073502e|Wed 2026-07-01 07:29:13 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6970-1201-1702431_14271078-0.service|simple|loaded|failed|failed|timeout|9956d8d8cc6a44b787c1e7ac80b7be7c|Wed 2026-07-01 07:29:18 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6976-4861-1708684_14249382-0.service|simple|loaded|failed|failed|timeout|678a247b14ef4f14bd9f764318c2abe4|Wed 2026-07-01 07:29:40 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6977-4862-1710899_14249525-0.service|simple|loaded|failed|failed|timeout|c953150bf4b240bcb9b0e8d02b0f0dee|Wed 2026-07-01 07:29:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6978-4863-1710911_14249526-0.service|simple|loaded|failed|failed|timeout|a038cccb9a914583bbd1a72ed4d0fb8c|Wed 2026-07-01 07:29:47 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6980-4864-1714882_14288051-0.service|simple|loaded|failed|failed|timeout|034eda29040e4f5f9a7f64e164f66ac6|Wed 2026-07-01 07:29:55 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6982-49993-1717481_14311660-0.service|simple|loaded|failed|failed|timeout|86ecb7b5396940308bf802009f67ff0b|Wed 2026-07-01 07:30:02 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6983-61979-1720238_14269410-0.service|simple|loaded|failed|failed|timeout|6c303dd25fee4aad811428e1d2777eb1|Wed 2026-07-01 07:30:07 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6984-61980-1720254_14265169-0.service|simple|loaded|failed|failed|timeout|55afc99ae07d4ea4b6c6cf14af5ef255|Wed 2026-07-01 07:30:07 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6985-46463-1722523_14300387-0.service|simple|loaded|failed|failed|timeout|7ffd6a488030492ea872a94c63487c07|Wed 2026-07-01 07:30:11 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6986-37689-1725971_14265464-0.service|simple|loaded|failed|failed|timeout|b42bb9c1aee6411eb84f9607f9d173a5|Wed 2026-07-01 07:30:18 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6987-37690-1725974_14265465-0.service|simple|loaded|failed|failed|timeout|81b87bb77c4349c39b819b169cb0a42a|Wed 2026-07-01 07:30:18 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6989-25558-1730150_14284685-0.service|simple|loaded|failed|failed|timeout|ccec7dbf4f694c7bae3452e097837674|Wed 2026-07-01 07:30:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6990-25559-1730172_14284686-0.service|simple|loaded|failed|failed|timeout|83dd43263845421bbdb5d9dc638c5bc2|Wed 2026-07-01 07:30:27 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6995-61981-1742130_14285323-0.service|simple|loaded|failed|failed|timeout|e3a5b154252c44248c13e379eb186032|Wed 2026-07-01 07:30:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6996-61982-1742188_14273643-0.service|simple|loaded|failed|failed|timeout|5c1be5336f6e4531a33b579985c347a6|Wed 2026-07-01 07:30:50 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6998-37691-1747937_14332622-0.service|simple|loaded|failed|failed|timeout|c69acb3211b748ee988bc8a7bbdba674|Wed 2026-07-01 07:31:02 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@6999-17126-1747983_14273996-0.service|simple|loaded|failed|failed|timeout|7ec6033c279f4b848f6c63a85046e371|Wed 2026-07-01 07:31:02 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7000-17127-1747998_14273997-0.service|simple|loaded|failed|failed|timeout|c4d732004ba1472aaebcaa69c8fccfff|Wed 2026-07-01 07:31:02 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7001-46464-1751364_14340713-0.service|simple|loaded|failed|failed|timeout|bc12f7c992224f2d94f0cb6ffeaf296c|Wed 2026-07-01 07:31:11 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7003-53722-1754266_14329628-0.service|simple|loaded|failed|failed|timeout|08b90a3a89c44727b20e9151a66a1205|Wed 2026-07-01 07:31:18 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7006-21007-2109364_14670106-0.service|simple|loaded|failed|failed|timeout|2773bde6b4e74531bb4abaf5af46d947|Wed 2026-07-01 08:00:44 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7007-21008-2109368_14670108-0.service|simple|loaded|failed|failed|timeout|e6da68e0721a4487926734ea7bdb3cba|Wed 2026-07-01 08:00:44 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7008-25560-2268293_14818004-0.service|simple|loaded|failed|failed|timeout|0f4afa73ce1e4f09a92526570b64ea90|Wed 2026-07-01 08:11:56 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7009-21009-2268662_14842823-0.service|simple|loaded|failed|failed|timeout|bb4289ea8b5a4ade9f2562c13e75d3f5|Wed 2026-07-01 08:11:58 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7010-25561-2268663_14842824-0.service|simple|loaded|failed|failed|timeout|da13df3508504acbbd9bf50c6afb20f3|Wed 2026-07-01 08:11:58 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7011-25562-2268664_14842825-0.service|simple|loaded|failed|failed|timeout|8704e5d9c98c4eeea679e39157b3480d|Wed 2026-07-01 08:11:58 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7012-1202-2360411_14884712-0.service|simple|loaded|failed|failed|timeout|ded95742455d4f9a90397d6ac68ccb18|Wed 2026-07-01 08:14:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7013-1203-2360413_14934969-0.service|simple|loaded|failed|failed|timeout|20b624077ef5473c9da29613f6a18168|Wed 2026-07-01 08:14:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@720-12411-80160_12639665-0.service|simple|loaded|failed|failed|timeout|226f8d7100504f54a1b5e74b30f16ce7|Wed 2026-07-01 05:54:10 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7666-50087-2555057_15119734-0.service|simple|loaded|failed|failed|timeout|5cf1386201df496ba4034a1d13153276|Wed 2026-07-01 08:26:07 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7703-33890-2560301_15148267-0.service|simple|loaded|failed|failed|timeout|2378882d05754598aa4b06e7c929e9b6|Wed 2026-07-01 08:26:34 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7793-53771-2575724_15119952-0.service|simple|loaded|failed|failed|timeout|1b3d3bb01b2d43d19a1c7d585276ca4d|Wed 2026-07-01 08:27:41 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7799-46632-2576411_15164282-0.service|simple|loaded|failed|failed|timeout|b664f8b967cf418db136b13049f57465|Wed 2026-07-01 08:27:45 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@7994-29906-2609547_15158314-0.service|simple|loaded|failed|failed|timeout|e4ca7b0cb7784796a001c9cbbce72aa0|Wed 2026-07-01 08:30:08 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@8046-46708-2618554_15158835-0.service|simple|loaded|failed|failed|timeout|915b8e404ab14340a04213b9f9345a54|Wed 2026-07-01 08:30:46 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@817-24697-94842_12615401-0.service|simple|loaded|failed|failed|timeout|cd279b43bea647bab66182db1c0c28f0|Wed 2026-07-01 05:55:21 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@8448-17260-2700791_15228771-0.service|simple|loaded|failed|failed|timeout|858681cf9a3e4ab984020a8cdb339420|Wed 2026-07-01 08:35:46 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@9054-1469-2780264_15361678-0.service|simple|loaded|failed|failed|timeout|88e6bcd84303415cb116c2c64ef9f227|Wed 2026-07-01 08:41:48 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 +drkonqi-coredump-processor@9247-13634-2793386_15349836-0.service|simple|loaded|failed|failed|timeout|eebe8182101f4afb976a39420b385ae6|Wed 2026-07-01 08:43:00 EDT|/usr/lib/systemd/system/drkonqi-coredump-processor@.service|2|15 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh index cd089e5..10d9f1d 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh @@ -23,6 +23,42 @@ check_shell() { return 0 } +capture_failed_unit_fingerprint() { + output=$1 + systemctl --failed --no-legend --plain --no-pager | awk '{print $1}' | LC_ALL=C sort | + while IFS= read -r unit; do + [ -n "$unit" ] || continue + printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ + "$(systemctl show "$unit" -p Id --value --no-pager)" \ + "$(systemctl show "$unit" -p Type --value --no-pager)" \ + "$(systemctl show "$unit" -p LoadState --value --no-pager)" \ + "$(systemctl show "$unit" -p ActiveState --value --no-pager)" \ + "$(systemctl show "$unit" -p SubState --value --no-pager)" \ + "$(systemctl show "$unit" -p Result --value --no-pager)" \ + "$(systemctl show "$unit" -p InvocationID --value --no-pager)" \ + "$(systemctl show "$unit" -p StateChangeTimestamp --value --no-pager)" \ + "$(systemctl show "$unit" -p FragmentPath --value --no-pager)" \ + "$(systemctl show "$unit" -p ExecMainCode --value --no-pager)" \ + "$(systemctl show "$unit" -p ExecMainStatus --value --no-pager)" + done > "$output" + return ${PIPESTATUS[0]} +} + +validate_failed_unit_disposition() { + (cd "$script_dir/failed-unit-disposition" && sha256sum -c SHA256SUMS) \ + > "$post_record/failed-unit-allowlist-checksum.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure 'failed-unit-allowlist-checksum'; return 0; fi + capture_failed_unit_fingerprint "$post_record/failed-units-fingerprint.psv" + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "failed-unit-fingerprint|rc=$rc"; return 0; fi + diff -u "$script_dir/failed-unit-disposition/drkonqi-failed-units.allowlist.psv" \ + "$post_record/failed-units-fingerprint.psv" > "$post_record/failed-unit-disposition.diff" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure 'failed-unit-disposition-changed-after-reboot'; fi + return 0 +} + validate_health_url_file() { file=$1 if [ ! -s "$file" ]; then add_failure 'health-urls|missing'; return 0; fi @@ -123,6 +159,7 @@ main() { printf 'HARD_STOP: run as a separate root process\n' >&2 return 10 fi + script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) if [ -z "$maintenance" ] || [ ! -d "$maintenance" ] || [ -L "$maintenance" ]; then printf 'HARD_STOP: supply the exact maintenance record path\n' >&2 return 11 @@ -169,7 +206,7 @@ main() { check 'running-kernel' running-kernel.txt uname -a check_shell 'installed-kernels' installed-kernels.txt 'pacman -Q linux linux-headers linux-zen linux-zen-headers' check 'failed-units' failed-units.txt systemctl --failed --no-legend --plain --no-pager - if [ -s "$post_record/failed-units.txt" ]; then add_failure 'failed-units-present'; fi + validate_failed_unit_disposition check_shell 'ssh-state' ssh-state.txt \ 'systemctl show sshd.service -p LoadState -p ActiveState -p SubState -p ExecMainStatus; journalctl -b -u sshd.service --no-pager' check_shell 'docker-daemon' docker-daemon.txt \ diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh index b6f79b0..25b5342 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh @@ -44,6 +44,75 @@ optional_shell() { return 0 } +capture_failed_unit_fingerprint() { + output=$1 + : > "$output" + systemctl --failed --no-legend --plain --no-pager | awk '{print $1}' | LC_ALL=C sort | + while IFS= read -r unit; do + [ -n "$unit" ] || continue + id=$(systemctl show "$unit" -p Id --value --no-pager) + type=$(systemctl show "$unit" -p Type --value --no-pager) + load=$(systemctl show "$unit" -p LoadState --value --no-pager) + active=$(systemctl show "$unit" -p ActiveState --value --no-pager) + sub=$(systemctl show "$unit" -p SubState --value --no-pager) + result=$(systemctl show "$unit" -p Result --value --no-pager) + invocation=$(systemctl show "$unit" -p InvocationID --value --no-pager) + changed=$(systemctl show "$unit" -p StateChangeTimestamp --value --no-pager) + fragment=$(systemctl show "$unit" -p FragmentPath --value --no-pager) + code=$(systemctl show "$unit" -p ExecMainCode --value --no-pager) + status=$(systemctl show "$unit" -p ExecMainStatus --value --no-pager) + printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ + "$id" "$type" "$load" "$active" "$sub" "$result" "$invocation" \ + "$changed" "$fragment" "$code" "$status" + done > "$output" + return ${PIPESTATUS[0]} +} + +validate_failed_unit_disposition() { + allowlist_dir=$script_dir/failed-unit-disposition + allowlist=$allowlist_dir/drkonqi-failed-units.allowlist.psv + allowlist_hashes=$allowlist_dir/SHA256SUMS + (cd "$allowlist_dir" && sha256sum -c SHA256SUMS > "$record/failed-unit-allowlist-checksum.txt" 2>&1) + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "failed-unit-allowlist-checksum|rc=$rc"; return 0; fi + capture_failed_unit_fingerprint "$record/failed-units-fingerprint.psv" + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "failed-unit-fingerprint|rc=$rc"; return 0; fi + cp "$allowlist" "$allowlist_hashes" "$record/" + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "failed-unit-allowlist-copy|rc=$rc"; return 0; fi + diff -u "$allowlist" "$record/failed-units-fingerprint.psv" > "$record/failed-unit-disposition.diff" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + add_failure 'failed-unit-disposition|set-state-or-invocation-fingerprint-changed' + fi + count=$(wc -l < "$record/failed-units-fingerprint.psv") + [ "$count" = 234 ] || add_failure "failed-unit-disposition|expected=234|actual=$count" + return 0 +} + +validate_snapper_readiness() { + btrfs subvolume show /.snapshots > "$record/snapshots-subvolume.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "snapshots-btrfs-subvolume|rc=$rc"; return 0; fi + stat -c 'path=%n|uid=%u|gid=%g|mode=%a|type=%F' /.snapshots > "$record/snapshots-stat.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "snapshots-stat|rc=$rc"; return 0; fi + owner_mode=$(stat -c '%u:%g:%a' /.snapshots 2>/dev/null) + [ "$owner_mode" = 0:0:750 ] || add_failure "snapshots-owner-mode|expected=0:0:750|actual=$owner_mode" + snapper list --columns number,type,pre-number,date,user,cleanup,description \ + > "$record/snapper-snapshots-before.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || add_failure "snapper-list|rc=$rc" + btrfs qgroup show / > "$record/btrfs-quota-observation.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + printf 'btrfs-quota-accounting|unavailable-or-disabled|rc=%s|snapshot-validity-not-affected\n' "$rc" \ + >> "$record/warnings.txt" + fi + return 0 +} + validate_health_urls() { : > "$record/health-urls.sanitized" if [ -z "${PUBLIC_HEALTH_URLS:-}" ]; then @@ -210,6 +279,7 @@ main() { return 10 fi umask 077 + script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) install -d -o root -g root -m 0700 /var/log/le-phase2a-preflight rc=$? if [ "$rc" -ne 0 ]; then @@ -245,7 +315,8 @@ main() { mandatory 'filesystem-inodes' filesystem-inodes.txt df -hi / /boot /var/cache/pacman/pkg /tmp mandatory 'installed-package-inventory' packages-before.txt pacman -Q mandatory 'failed-unit-inventory' failed-units-before.txt systemctl --failed --no-legend --plain --no-pager - if [ -s "$record/failed-units-before.txt" ]; then add_failure 'failed-units|present'; fi + validate_failed_unit_disposition + validate_snapper_readiness mandatory_shell 'kernel-initramfs-inventory' kernel-initramfs-before.txt \ "find /usr/lib/modules /boot -maxdepth 2 -type f \\ \( -name vmlinuz -o -name 'vmlinuz-*' -o -name 'initramfs*' -o -name 'initrd*' -o -name pkgbase \) \\ @@ -277,10 +348,10 @@ main() { "find /etc -xdev -type f \( -name '*.pacnew' -o -name '*.pacsave' \) -printf '%p\\n' | sort" mandatory_shell 'migration-git-state' migration-git-state.txt \ 'git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration status --short --branch; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse HEAD; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse origin/migration/le-app-database' - mandatory_shell 'preview-report-integrity' preview-report-checksum.txt \ - "printf '%s %s\\n' 195d74afe121556294318d0ed49e3fb5504088dbb31a4c0786b8adea44f1369d /tmp/le-phase2-package-decision-20260712.Mn7mIx/phase2-package-decision-report.md | sha256sum -c -" + mandatory_shell 'approved-transaction-integrity' approved-transaction-checksum.txt \ + "cd '$script_dir/approved-transaction' && sha256sum -c SHA256SUMS" - inventory_files='packages-before.txt failed-units-before.txt kernel-initramfs-before.txt bootloader-inventory.txt containers-all.psv running-containers-before.txt critical-units-baseline.psv database-baseline.psv database-readiness-before.txt health-urls.sanitized public-health.txt network-firewall-before.txt pacman-Dk-before.txt pacman-Qkk-before.txt pacman-Qkk-missing-or-unreadable-before.txt pacnew-pacsave-before.txt migration-git-state.txt preview-report-checksum.txt' + inventory_files='packages-before.txt failed-units-before.txt failed-units-fingerprint.psv failed-unit-disposition.diff failed-unit-allowlist-checksum.txt snapshots-subvolume.txt snapshots-stat.txt snapper-snapshots-before.txt btrfs-quota-observation.txt kernel-initramfs-before.txt bootloader-inventory.txt containers-all.psv running-containers-before.txt critical-units-baseline.psv database-baseline.psv database-readiness-before.txt health-urls.sanitized public-health.txt network-firewall-before.txt pacman-Dk-before.txt pacman-Qkk-before.txt pacman-Qkk-missing-or-unreadable-before.txt pacnew-pacsave-before.txt migration-git-state.txt approved-transaction-checksum.txt' ( cd "$record" || return 1 sha256sum $inventory_files > inventory-hashes.sha256 From a9fec0d83d1166c3424479bb53ba68048127b15a Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 18:59:45 -0400 Subject: [PATCH 09/14] add Phase 2A container and recovery readiness gates --- .../phase2a-host-maintenance/MANIFEST.sha256 | 18 +++- .../phase2a-host-maintenance/README.md | 34 ++++++- .../backup-readiness-plan.md | 94 +++++++++++++++++++ .../container-disposition/README.md | 29 ++++++ .../container-disposition/SHA256SUMS | 6 ++ .../container-disposition/all-containers.psv | 88 +++++++++++++++++ .../directus-stopped.allowlist.psv | 1 + .../restart-policy-no.psv | 2 + .../running-containers.psv | 87 +++++++++++++++++ .../sonarr-unhealthy.allowlist.psv | 1 + .../execute-maintenance.sh | 48 +++++++--- .../grub-btrfs-readiness.md | 78 +++++++++++++++ .../maintenance-worksheet.md | 32 +++++++ .../post-reboot-validate.sh | 74 +++++++++++++-- .../preflight-read-only.sh | 67 ++++++++++++- 15 files changed, 629 insertions(+), 30 deletions(-) create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/backup-readiness-plan.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/README.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/SHA256SUMS create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/all-containers.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/directus-stopped.allowlist.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/restart-policy-no.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/running-containers.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/sonarr-unhealthy.allowlist.psv create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/grub-btrfs-readiness.md create mode 100644 docs/le-app-database-migration/phase2a-host-maintenance/maintenance-worksheet.md diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 index d583d39..cc09462 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 +++ b/docs/le-app-database-migration/phase2a-host-maintenance/MANIFEST.sha256 @@ -1,4 +1,4 @@ -9934efd3946efad746823ebbeb46875cc0ae1e14b5b2b3e399a384c3654bcd2a README.md +d74bbe90e7b024bc650c34e4613678d7cae67ff56cb4bde721a22362bdda33b1 README.md 28ebc7e3467d2d83539db1b75824e38b7164fbb940162064d7a9b6af50223257 approved-transaction/README.md 6991258799bec838137470e0b9b71ed97169bbdf157896fce3e38b7ebb7d3361 approved-transaction/SHA256SUMS 8182c9bf8b4c6830a2b930698e2e8f5fb7e5ee7849fc8500a8b4813b9292ad51 approved-transaction/backups.txt @@ -12,11 +12,21 @@ d5711f5d3ee9ae38c336b7ae02c036b6927d1e422d72ad8a1f832dae92c41337 approved-trans 84cf71dd5da459699825c4d25975e15f58bde8fd7cfdb2839bd82441271daaed approved-transaction/scriptlets.txt f5661dd480a24e6c7814fff9c0765688a4b16309176e97efa5f3e0cb10d6d05d approved-transaction/totals.psv 88252b0d5eaafcdb69a1865b575d44636be203075d996fad2ea6e57b2810150a approved-transaction/upgrades.psv -1a427dc140c990a6d23f1c77eeef4566d193c32d6ed1e6e4d16f7c49d5b9e67f execute-maintenance.sh +17323251ae0c0d37b2116551846866f7f4352cca2e6039be52f2b2831c4ca865 backup-readiness-plan.md +64269e6af4b6c668737ec4f5ec4dec6a49a527a256a62644dde80b2fa356145c container-disposition/README.md +a98eaa5efea0d5f4a22311ff89c6055979855a4b4cf53e1e9eb5e18d08da1b54 container-disposition/SHA256SUMS +1d9e10136d127798aa1f834ad27d6d3d81b09fb585d78aaccb869a564065b19f container-disposition/all-containers.psv +bca981ad79a7bb84753112f13698ebf9b72471db29c2d4096f76f72898ace6d8 container-disposition/directus-stopped.allowlist.psv +f6d68530714567fa8e3d7391dc7eb5232f3343be5d419fb24df56c552a7cebc6 container-disposition/restart-policy-no.psv +c635776b21da4324ee0279be026ce8b5971d516cc4573313ad153db634ad4c1a container-disposition/running-containers.psv +75634105ec7afcb8009c9159be3fc5631cd9993dbca131c4a651515c4244b486 container-disposition/sonarr-unhealthy.allowlist.psv +aef1864fa108e0d8cd4af5c513f6c814d0fa24c7fb81fd7ba554ece9d895af19 execute-maintenance.sh e4667c83bd4e9e7de413e48cd16cc2d4b041cb09fbadace5e9f8d74a73a5a2a1 failed-unit-disposition/README.md e4f82fbefec4bc8c91f473ead9a70d0d0b1a4a14c54a46067127ac05cf4a6dcc failed-unit-disposition/SHA256SUMS 217a0fe59a3bff6c6621bcaac59dcad1c74154486cd7e75e3bba83b6909c29c4 failed-unit-disposition/drkonqi-failed-units.allowlist.psv -f3c3813cf2a7f119f010da956dba1bc25b8986b9e3acaa8eb91fcff95f7ec8c0 post-reboot-validate.sh -0753237d8d42248b18b26cc076782816fbcc5f879133fc0231b6c0902d6f9c4f preflight-read-only.sh +17f536533aca7e0542474d91da255a25889e96c5421a6a659d26a213dfb1653a grub-btrfs-readiness.md +f163eca74ff5e288b0638af728072452a614ddc4430b91b70cab1429f99535eb maintenance-worksheet.md +aabae1940542707d197388488055987c35e4f0c05621e68993ff3f863563f096 post-reboot-validate.sh +1afa76fd1444c408c95a2740fbe39c966d68bd69e0becd01ee7931ffef9be7ff preflight-read-only.sh 5b88c68da055d3578e8b19fdf7623c3de204d5ef0af4b17e113938d8bd0a2c5e reboot-procedure.md 0805397870c66a523f693cfdba33f7b08c94793b62ddadf720ff84c22796ea13 rollback-recovery.md diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/README.md index 4020a4b..70b1479 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/README.md +++ b/docs/le-app-database-migration/phase2a-host-maintenance/README.md @@ -33,7 +33,7 @@ Before execution, record site-specific values in the maintenance ticket: URLs can be supplied without editing the scripts: ```bash -export PUBLIC_HEALTH_URLS='https://example.invalid/health https://example.invalid/' +export PUBLIC_HEALTH_URLS='https://db.lasereverything.net/' ``` Health URLs must be non-secret public HTTPS URLs. The scripts reject user-info credentials, query strings, fragments, whitespace/control characters, and non-HTTPS schemes. Never place tokens, signed parameters, credentials, or private endpoints in `PUBLIC_HEALTH_URLS`; the scripts do not read environment files to discover URLs and do not log redirected effective URLs. @@ -51,6 +51,9 @@ export LE_PHASE2A_RECOVERY_READY=YES export LE_PHASE2A_BACKUPS_READY=YES ``` +Both attestations remain unset in `maintenance-worksheet.md`. Do not set them +until independent recovery access and the proportional backup plan are complete. + ## Invocation and enforced records Invoke every script as a separate process, never with `source` or `.`. Explicitly capture its return status: @@ -108,6 +111,28 @@ addition, removal, state or invocation change, checksum failure, or other failed unit is a hard stop. The scripts do not reset them. `nxserver.service` is not an expected critical unit; its unused `nomachine` package was removed normally. +## Docker disposition and authoritative baseline + +The checksummed `container-disposition/` baseline contains exactly 88 containers: +87 running, retired `directus` intentionally stopped, and `sonarr` as the sole +unhealthy running container. Sonarr is a reviewed unrelated pre-existing fault; +its empty configuration, absent port 8989 listener, and failing health check do +not require repair for Phase 2A. Any identity/state drift or any additional +unhealthy container invalidates preflight. + +All 87 running containers are authoritative for post-reboot validation. Healthy +containers must return healthy; containers without health checks must return +running. Sonarr may remain unhealthy or improve, but must remain present and +running. Directus must remain stopped and unchanged. `castopod` and +`castopod-redis` use restart policy `no`; failure to return is recorded as a +validation failure requiring explicit operator attention, never an automatic +start or restart. + +The protected Directus archive at `/srv/directus-archive/20260710-175408` +verified successfully against its actual `SHA256SUMS` with return code 0. +Preflight repeats that check silently with `sha256sum --status -c SHA256SUMS` and +hard-stops on failure without printing archive contents. + ## Snapper readiness history and maintenance gate On 2026-07-12 the ordinary empty `/.snapshots` directory was replaced by Btrfs @@ -132,13 +157,18 @@ The older preview remains at: It occupies approximately 2.4 GiB and must not be deleted yet. It is historical, not the approved baseline. Cleanup remains a later explicit action. +GRUB/grub-btrfs findings and the unexecuted standalone generation procedure are +in `grub-btrfs-readiness.md`. Snapshot coverage, external subvolumes/mounts, and +the minimum application-consistent database/configuration backup set are in +`backup-readiness-plan.md`. + ## Hard stop conditions Stop rather than improvise if any of the following occurs: - no independent recovery access or root path; - insufficient space or an unhealthy `/boot`; -- degraded storage, failed backup, unresolved failed units, or unhealthy production service; +- degraded storage, failed backup, undispositioned failed units, or an unhealthy production service/container other than the exact reviewed Sonarr disposition; - unexpected package count/version, downgrade, repository error, signature error, keyring error, mirror inconsistency, or partial transaction; - SSH access is unstable before the OpenSSH restart hook; - DKMS/initramfs/kernel/bootloader generation fails; diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/backup-readiness-plan.md b/docs/le-app-database-migration/phase2a-host-maintenance/backup-readiness-plan.md new file mode 100644 index 0000000..e1af320 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/backup-readiness-plan.md @@ -0,0 +1,94 @@ +# Proportional Phase 2A backup-readiness plan + +## Verified existing evidence + +- `/srv/directus-archive/20260710-175408/SHA256SUMS` verified with return code 0. +- The repaired `/.snapshots` Btrfs subvolume supports Snapper; snapshots 1–3 + succeeded. +- Maintenance creates and directly verifies a uniquely numbered pre-upgrade root + snapshot before Pacman. + +These facts do not yet justify `LE_PHASE2A_BACKUPS_READY=YES`. + +## Snapshot coverage + +The explicit root snapshot is crash-consistent coverage of the root `@` +subvolume, including ordinary paths below it such as `/etc`, `/usr`, `/boot` +when `/boot` is not a separate mount, and `/var/lib/docker` unless independently +mounted. It is not an application-consistent database backup. + +The following are outside root `@` and therefore outside that snapshot: + +- Btrfs subvolumes `@home`, `@root`, `@srv`, `@cache`, `@log`, and `@tmp`; +- the EFI system partition mounted at `/boot/efi`; +- ext4 mounts `/mnt/storage`, `/mnt/appdata`, `/mnt/media`, and + `/mnt/coldstasis`; +- tmpfs/runtime mounts. + +The protected Directus archive is below `/srv`, so its independent checksum +verification—not the root snapshot—protects that evidence. Bind-mounted database +or application state on the separate subvolumes or ext4 mounts is likewise not +covered by the root snapshot. Unrelated media need not be duplicated merely +because a consuming container is running. + +## Minimum fresh package-maintenance backup set + +Before setting the backup attestation, create and test: + +1. application-consistent dumps of every active MariaDB/MySQL and PostgreSQL + production database, including the transitional `le-directus-source` and + `le-payload-pg` databases if they must survive the maintenance window; +2. a root-only host-configuration archive containing package inventory, `/etc`, + GRUB configuration, EFI boot metadata, relevant systemd units/drop-ins, + nftables/network configuration, and Docker Compose/deployment definitions; +3. the explicit root Snapper snapshot already enforced by maintenance; +4. checksum manifests plus a documented restore test or validated restore + procedure for the new dumps and configuration archive. + +Do not copy unrelated media libraries or bulk application media solely because +their containers are running. Database dumps provide application consistency; +the Btrfs snapshot provides fast crash-consistent package/config rollback. + +## Review-only commands + +The operator must replace the uppercase placeholders with reviewed root-only +paths. Credentials must come from existing root-only client configuration or an +interactive mechanism; never place passwords in command arguments or logs. + +```bash +stamp=$(date -u +%Y%m%dT%H%M%SZ) +backup_root=/ROOT/ONLY/BACKUP/DESTINATION/le-phase2a-$stamp +install -d -o root -g root -m 0700 "$backup_root" + +pacman -Q > "$backup_root/packages-before.txt" +cp --preserve=mode,ownership,timestamps /boot/grub/grub.cfg "$backup_root/grub.cfg" +efibootmgr -v > "$backup_root/efibootmgr.txt" + +tar --xattrs --acls --numeric-owner -cpf "$backup_root/host-config.tar" \ + /etc /usr/lib/systemd/system /boot/grub + +mariadb-dump --defaults-extra-file=/ROOT/ONLY/MARIADB-CLIENT-CONFIG \ + --all-databases --single-transaction --routines --events --triggers \ + > "$backup_root/mariadb-all.sql" + +pg_dumpall --file="$backup_root/postgresql-all.sql" + +# Transitional containers: use existing non-secret local authentication only. +docker exec le-directus-source mariadb-dump \ + --defaults-extra-file=/ROOT/ONLY/IN-CONTAINER-CLIENT-CONFIG \ + --all-databases --single-transaction --routines --events --triggers \ + > "$backup_root/le-directus-source.sql" + +docker exec le-payload-pg pg_dumpall \ + > "$backup_root/le-payload-pg.sql" + +find "$backup_root" -maxdepth 1 -type f -print0 | + LC_ALL=C sort -z | + xargs -0 sha256sum > "$backup_root/SHA256SUMS" + +(cd "$backup_root" && sha256sum -c SHA256SUMS) +``` + +These commands are a review package, not execution approval. Confirm database +names, authentication methods, output capacity, dump return codes, nonempty +outputs, and a restoration test before attesting readiness. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/README.md b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/README.md new file mode 100644 index 0000000..22609f2 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/README.md @@ -0,0 +1,29 @@ +# Reviewed Docker container disposition + +Source: root-created, checksum-verified read-only baseline +`/tmp/le-phase2a-container-baseline.HyAW6sPg`, captured 2026-07-12 without +container environments, mounts, secret files, or credentials. + +The baseline contains 88 containers: 87 running, one intentionally stopped, and +one unhealthy. `sonarr` is the sole reviewed unhealthy running container. Its +exact container ID, image reference and image ID, running/health state, startup +timestamp, network mode, restart policy, Compose project, and hash of its health +check definition are in `sonarr-unhealthy.allowlist.psv`. The operator verified +that its `/config/config.xml` is empty, no listener exists on port 8989, and its +health check fails. Sonarr is unrelated to Phase 2A and need not be repaired. +Any identity change, unexpected state, or additional unhealthy container is a +hard stop before maintenance. + +`directus` is the sole stopped container. It is retired and must remain stopped; +its exact identity is in `directus-stopped.allowlist.psv`. Starting, recreating, +replacing, or changing its recorded state invalidates preflight. + +`running-containers.psv` is the authoritative 87-container post-reboot baseline. +Containers recorded healthy must return healthy. Containers without health +checks must return running. Sonarr must remain present and running; its health +may remain unhealthy or improve to healthy. It may not become missing, stopped, +restarting, or dead. No script starts, restarts, or recreates a container. + +The running `castopod` and `castopod-redis` containers have restart policy `no`. +If either fails to return after reboot, validation records explicit operator +attention and fails without starting it automatically. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/SHA256SUMS b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/SHA256SUMS new file mode 100644 index 0000000..819a3ab --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/SHA256SUMS @@ -0,0 +1,6 @@ +64269e6af4b6c668737ec4f5ec4dec6a49a527a256a62644dde80b2fa356145c README.md +1d9e10136d127798aa1f834ad27d6d3d81b09fb585d78aaccb869a564065b19f all-containers.psv +bca981ad79a7bb84753112f13698ebf9b72471db29c2d4096f76f72898ace6d8 directus-stopped.allowlist.psv +f6d68530714567fa8e3d7391dc7eb5232f3343be5d419fb24df56c552a7cebc6 restart-policy-no.psv +c635776b21da4324ee0279be026ce8b5971d516cc4573313ad153db634ad4c1a running-containers.psv +75634105ec7afcb8009c9159be3fc5631cd9993dbca131c4a651515c4244b486 sonarr-unhealthy.allowlist.psv diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/all-containers.psv b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/all-containers.psv new file mode 100644 index 0000000..d4db80b --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/all-containers.psv @@ -0,0 +1,88 @@ +bazarr|0c752272254cfad9aeb5a6d981a8d354c0676a18eb14008b4613c7a442ea995c|linuxserver/bazarr|sha256:71677d1a237182abf46f607d346109c5a78aae3973d01b143cd89f39569345fa|running|no-healthcheck|2026-07-02T16:22:04.651521486Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +bgbye|2d7d7a1250b29f5e66c3068a4077e2d3df801de3f0c1802882aae1e39a84b05e|lasereverythingnetdb-bgbye|sha256:fa4994a5386cd97cba309b273025478722b06a8985dae249bb80c50a170355ce|running|healthy|2026-07-02T16:23:13.15908777Z|makearmy_net|unless-stopped|lasereverythingnetdb|healthcheck_sha256=6e5884b66db6e8cc64ccec2fc861dc2ee21b1ffce0b8555fdffd69811f89eb3a +buildx_buildkit_defaultbuilder0|6b0affe65f5e3589baa0d57169713a1d9ffa5df5c9f6484a8e32bc4070d67ee6|moby/buildkit:buildx-stable-1|sha256:fe0990fb85c4586aaa94e599905b75a2676664f065f29bea67ebcd5b2fe88acb|running|no-healthcheck|2026-06-30T17:45:39.545876028Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +castopod-redis|45094b8a921517979c9a8a519c7ff2fc71a540656f2309ffbd0696cf56bf5dcf|redis:7.2-alpine|sha256:b6636bae9624cba73c69fc9d21318151217a103a1db600b8012db8c460785c26|running|no-healthcheck|2026-07-02T16:21:44.78699982Z|castopod_ma_net|no|makearmyiopodcast|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +castopod|b3612901d76d0b2be752762fb1e5d49ca7b521a648ab9b6f332ff7ace61ba54a|castopod/castopod:latest|sha256:d624bd1d45a4b929b9202cae2c4290cdf2624b6bf172fe2115ce20bdb359a428|running|healthy|2026-07-02T16:21:44.790327595Z|backend_net|no|makearmyiopodcast|healthcheck_sha256=a8def15088dacf1eb21b7a57747b2113566689d9876c5d690d7c3269562ec5cc +collabora|bcf6738bab2b22f3db163f88f28608548a748a49e6088842e6e687a968c65371|collabora/code:latest|sha256:bf688dedd8778797a551ef5f768c33993cbecc23b9d68527176a59db5c7726c0|running|no-healthcheck|2026-07-02T16:22:15.786914258Z|collabora_s6_net|always|s6vinetoffice|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +continuwuity|a22c7cef0512cefc66441cc99d2b6f4ce36c48e70177c04dfdf88a44e91396ad|forgejo.ellis.link/continuwuation/continuwuity:latest|sha256:f45ecc72598a18e69e16fa7addbb5300b6eba7cd1bf0bf423a8d0b834c8c7abf|running|no-healthcheck|2026-07-02T16:21:42.218988545Z|conduit_net|unless-stopped|makearmyiomatrix|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +dcm|1d3291029cb4f3850e43ea93d6bc3a3ceb1d12ecfea27f136b99c3af61e65d75|ghcr.io/ajnart/dcm|sha256:2a6935c5032df59d0986753a05250dacfd66522554471fd54b0d302a67d6de98|running|no-healthcheck|2026-07-02T16:21:22.600579243Z|makearmyiodcm_default|unless-stopped|makearmyiodcm|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +directus|c65e7651bfb8827b712f3ed57fff5839dc5cdc87f9f416b7a09f389c7864a1a3|directus/directus:latest|sha256:dd5537b4b35a4a980452e8160def97e63bfd0a6b92e6d81ec980a3300801ce2a|exited|no-healthcheck|2026-07-02T16:21:21.312699245Z|backend_net|no|lasereverythingnetforms|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +ergo|f0d41caf4e9ec55b0bf677d8ba622660e918c30094b643cdf09926df545ca4cb|ghcr.io/ergochat/ergo:stable|sha256:ca8d10822089ef9fd05ba5cc18049b687bc46e2e0cc5901aed4bf92da337a7a0|running|no-healthcheck|2026-07-09T17:20:10.183703547Z|irc_net|on-failure|makearmyioirc|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +flaresolverr|3ce787aacca3e92e836526c81c7e6fab2dcb7d810b04fbf521e5deba2fd3039f|ghcr.io/flaresolverr/flaresolverr|sha256:894893ed4d4557764df3e74d27c5bc1b3944e5a33ed410e62ab7c744b348f415|running|healthy|2026-07-02T16:22:04.654391107Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=5b79b0ceb45d2b3ea48a3a1ab49a3c8a7ab3d42b98e63a185986126b8f6e5b71 +forgejo|1befcad10bf9b56880c9e2b223e2dd6e0c5dd500d1453a83768e974ced54273d|codeberg.org/forgejo/forgejo:11.0.1|sha256:49388d84630e37520e7c5d4971dd94564a7a39f18f66b60d40c32b66f7ff40b9|running|no-healthcheck|2026-07-10T00:57:13.158697179Z|backend_net|unless-stopped|makearmyioforge|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +heimdall|c8bab4edf73f1265ab91dd384d8942562827f8ce2185fb65e398c8e923872e13|lscr.io/linuxserver/heimdall:latest|sha256:96a3814546b9465dbee1cce1c9547bed5b6f1a13ca751f239cf88502f396df67|running|no-healthcheck|2026-07-02T16:21:46.899952815Z|heimdall_s6_net|unless-stopped|s6vinet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +immich_machine_learning|2b6046557c314400dec23f1caae69eae07a8e07542db79957f7b7e86d41aaff6|ghcr.io/immich-app/immich-machine-learning:release|sha256:088b410d324842faeeeb69d0d5fef6b3eccee3f6fe3c68a2b1235649a86872c4|running|healthy|2026-07-02T16:22:16.356336193Z|backend_net|always|immich|healthcheck_sha256=06fc5259e91d939c46d4cf740394a5057ea58131e5df6789d7e4f5ebbb6d7a59 +immich_postgres|17889bb81cea5af9cf6ac6bafe6eba61aeb8d50cb2c0996c699560182796945e|docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52|sha256:3062a7ddd658eee20fcc428853dd34c23c7ad95fb48e3181922ffef11217389a|running|healthy|2026-07-02T16:22:16.362765814Z|immich_s6_net|always|immich|healthcheck_sha256=3b85ca1ca42325d6e6d9501e2adcf04a7a80dcb42159b69d2527d6432473ff32 +immich_redis|a2d1a7bc973927e404a2576cb3de755a890f7ded3f3361a8a065eab88a0d481c|docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8|sha256:6dd588768b9ba229530de57c108536a8d6b98d53d83f217f59167981fda443ca|running|healthy|2026-07-02T16:22:16.364329237Z|immich_s6_net|always|immich|healthcheck_sha256=bd98ba333892e944c8245cd358b2946469adf44d3d8ec7cabb936197e4362807 +immich_server|2115244bab14fec49130c4a3949dfc2773871fc54928f88be963388e5e04d790|ghcr.io/immich-app/immich-server:release|sha256:de80bec4fc193605671b0f9600fe6b20f598fd2cb503ffe1a6e67f7cfd0e5233|running|healthy|2026-07-02T16:22:16.558556945Z|backend_net|always|immich|healthcheck_sha256=b7dce503cff869bbffb63034400837e1ad79981eed7722359480a50dbc96436f +jellyfin|ba3ef59b346479f17f4c2b9fd33828db2638801e408fca115e5289b371f89eaf|jellyfin/jellyfin:latest|sha256:d57d4a0c18b3e51b913bd2f8f0f122eb7c7150ea37bb0e352df9cb329de11d24|running|healthy|2026-07-02T16:22:17.374948578Z|jellyfin_s6_net|unless-stopped|s6vinetwatch|healthcheck_sha256=a22856b3a5dfad87ecc898383d5b49daa6b4b98bca2797ea36cb422e824de47c +jellyseerr|c00ff6cc7213669b85bc8e28a51ffc16779d2489c7378f3911735b8d983dcd05|fallenbagel/jellyseerr|sha256:2742757d9c41bcb4acb76c86c4ce23a8c54d5dbe93a698c815a9a34bed0b18d0|running|no-healthcheck|2026-07-02T16:22:04.637037538Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +kavita|578e4aa1ceeb4eb61ce094ecc862cdce7d68538a2c3e7b17cb29c3f4798597d4|jvmilazz0/kavita:latest|sha256:12ac333d6fde07c087a135dd23fa5b930a96e2a3fde9cfbb281214f040b4cbba|running|healthy|2026-07-02T16:22:04.630545121Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=256947cdccf855523b654955b26538932c2ee76e01f507ef885f1e885437ddb4 +lasereverything-manager-bus-core|791f2ef47fc1c798318b4136773672dc0f8ff08cd104f075f2f317ca0caf1817|ghcr.io/true-good-craft/tgc-bus-core:latest|sha256:e053cb452820daba308e86f456ba7d1e2b0306f058eae2c91ee7122a8634f4bb|running|healthy|2026-07-02T16:21:21.581810214Z|lasereverythingnetmanager_default|unless-stopped|lasereverythingnetmanager|healthcheck_sha256=5f70654d32c1e7e9060a6069ae4e3e8d1b94c196cd73a66ae40c93098344ec59 +lasereverythingnettv-radio-1|297e17de84c4d3c31f9cdeda9dcbefe185e6ec6c48c44c7fdae9f3e3f5ce982c|jrottenberg/ffmpeg:latest|sha256:1cf434af29091eba2d80108e28864540dc98bd3e4e5cda2b497e758d60bacba1|running|no-healthcheck|2026-06-30T17:48:55.163034213Z|lasereverythingnettv_default|unless-stopped|lasereverythingnettv|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lasereverythingnettv-tv-1|776624328baf74b4196474c0962d2e01b1caa21e8bc3a2f6c77aece135faf30e|jrottenberg/ffmpeg:latest|sha256:1cf434af29091eba2d80108e28864540dc98bd3e4e5cda2b497e758d60bacba1|running|no-healthcheck|2026-06-30T17:48:55.161961872Z|lasereverythingnettv_default|unless-stopped|lasereverythingnettv|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +le-directus-source|6adbd503febfd8a6236fc7c941df7bfc748e76dcd510c11e69a3ff02776ff344|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|running|no-healthcheck|2026-07-10T23:15:59.628507071Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +le-payload-pg|2ee8f491397b5cbab595121f0e5e57fa5778329090d27a80b002bdd83a5ee8f2|sha256:639c86b6cacfdf788c075ffb5879286aa0e905965a4832ba6c76ccb0bca9417b|sha256:639c86b6cacfdf788c075ffb5879286aa0e905965a4832ba6c76ccb0bca9417b|running|no-healthcheck|2026-07-10T23:20:42.20954498Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-pictrs|22597c5b8ac30d2d715bd538ee15e27e07d8677c9a191c2af741c57b1adbe08d|asonix/pictrs:0.5.17-pre.9|sha256:9a5411f20149156b2bbbcc69b63918e9259d8d4653244eae7482e71177e1b15a|running|no-healthcheck|2026-07-02T16:21:41.143484739Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-proxy|f267f47868f6bd4c0f2b3594c1b23909862fbe853491cd514e9f8205be3499d2|nginx:1-alpine|sha256:ea51152ef8c480b999cee0271a288c698704b18483276119b1253e576ef3232f|running|no-healthcheck|2026-07-02T16:21:41.4297012Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-ui|e53db215d6385a424365263fbce8edd9d08f1ea0927bc0aa136af4694ba22d89|dessalines/lemmy-ui:0.19.12|sha256:0a85b3192f29a5b14d41c265d9d5cab60bd281b4a5b5db8f3a85d5d9bf01aaaa|running|healthy|2026-07-02T16:21:41.334389805Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=fe44578c9a0ef5c3150b372caa763f18afad8938c28752af3570a87d8deb88e4 +lemmy|1f032d67308df68711b5d432398ff31094da65dbdddbb966e60d745390fd0a0d|dessalines/lemmy:0.19.12|sha256:68c00173ade225585ea7d54ed2688f8f23d7d2886ae3f5b4eefd70dbc05b044a|running|no-healthcheck|2026-07-02T16:21:41.230573992Z|backend_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mailu-admin|ed54ee705edfd3867b98548c7a5054ba8e58eb8fc3f7afbf3e0d5378fe508f4e|ghcr.io/mailu/admin:2024.06|sha256:e4319dc05cd8ee8204ebb0343685d52a9c645f192f92a9ba24e05a702a722894|running|healthy|2026-07-10T15:30:56.858313278Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=65c193d7ce37216310a9ad7324433501d4532a0e95241ba91a6643556dcb2b8d +mailu-antispam|0f2bc2ca4ee1971e3dfdd9534ffa5ffa6bb5b1530f8c1e8c595a7c1303d70cba|ghcr.io/mailu/rspamd:2024.06|sha256:a8619d7f452f04d806b1f334e3e28e123c5f568080e168051fe98c7b3637925d|running|healthy|2026-07-10T15:30:57.245953555Z|arrmailnetmail_clamav|always|arrmailnetmail|healthcheck_sha256=7ffc0eff154ba7ec4712f405258f56347f428b7f2ec716554a9f6c1fed3b896d +mailu-antivirus|22b05b37de3374f615e15e79cee3632e3f2398a2c50a5183a3baa94add519b62|clamav/clamav-debian:1.4|sha256:f8a1f4389e5cbfb43ffc839d751bf746c6c27f5de780ba1fcde15d596f94287b|running|healthy|2026-07-10T15:30:56.621336608Z|arrmailnetmail_clamav|always|arrmailnetmail|healthcheck_sha256=713e2ecbe297d1e71fa7f1819859d65ad738041705100f5c4ae73f4ae551fb60 +mailu-fetchmail|a47a586ae5cdb3d810932c4dfe06ec242ec02cc3999f7ecec2fac1779cf14ffc|ghcr.io/mailu/fetchmail:2024.06|sha256:eccbdf53fb66dc34a49bde349f53eb9d1bba60c98a1be5d9eabe68a726f13e6a|running|healthy|2026-07-10T15:30:57.460051545Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=001e1e50755b8a7ad162dfd92611a0d0944aa07d8ca6dc19ebffb0c60494878e +mailu-front|a82753aefc6c8243c7c71caa0ef289a1a2e83c3fb1f9e93e779e56267353d11c|ghcr.io/mailu/nginx:2024.06|sha256:30a4f996f255fe498ed847b9a3759efd45fc5d8bd5c4dc121896e83615e60b0d|running|healthy|2026-07-10T15:30:56.726639815Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=c379c13a800f5417c0692896be411b09f7fe5a95d530f676abfd7d2dae1ea24c +mailu-imap|549c48f83f429fd9b1e52f7752012b1250107dc511c1b7ee94ac2a49b521a726|ghcr.io/mailu/dovecot:2024.06|sha256:27aceac0272f97361a5a5b1d5abea7099e10f409e3d28a6096f9111fe90bf347|running|healthy|2026-07-10T15:30:57.247440245Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=9462f6ccccab8ea58f34afb7cfaae178b2d36f27a29e903b89ea4a7a1ed73cd6 +mailu-oletools|841c57f99cfeec974e757666484b8c1a37ca0f2696d719d49317294253edb33b|ghcr.io/mailu/oletools:2024.06|sha256:76fb07165f54411c1136a449ba2963fe11618f0cf4629fdc13eadd628d3528c9|running|healthy|2026-07-10T15:30:56.729115113Z|arrmailnetmail_oletools|always|arrmailnetmail|healthcheck_sha256=b1b33aade32e78a9484d43059f9f651eef8a2e8b8910893a4436f15a078abd08 +mailu-redis|4f1b06b40d4ebe43ee8391c91d0c02645a7f23dc91229123a0a25e9098eb567d|redis:alpine|sha256:55563f45704d59e891b6aa643014bb6b562abf21909a44b3b297502415b68e29|running|no-healthcheck|2026-07-10T15:30:56.720208042Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mailu-resolver|96d3aaea44a0a8779c2a11fd7cb341bbc9aa23ad879381c5f7a247a0554cc444|ghcr.io/mailu/unbound:2024.06|sha256:d30593c8bf42bfd1c46bd1ac7ee90862b0cfa7e96fd10da1dacc2c69cafb443d|running|healthy|2026-07-10T15:30:56.617962677Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=5b9b365275d40b354506f2b02070d7785700a374ee09de8d7ee8527bbbf1b16f +mailu-smtp|22e3c459d26b808ffca118df679aae2c9cc511de3fcaba4a9847770df6f24de5|ghcr.io/mailu/postfix:2024.06|sha256:0bed674f22e609e0b2f6780865097a41ed6ca5307db4bcc0794a35e2e7e976c9|running|healthy|2026-07-10T15:30:57.241512918Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=9b234890e871375cec386d4edacf17bcbd8a5504fab8b1177d956c56622a08aa +mailu-tika|60ba021f422056617055244f608c881f5ce282c2d1590e52d8d2719a44b1eb98|apache/tika:2.9.2.1-full|sha256:e646191162f8cb7aef0b512d54c028f62bd86e0e5f53b314ab82cfac7ef292d7|running|healthy|2026-07-10T15:30:56.723391978Z|arrmailnetmail_tika|always|arrmailnetmail|healthcheck_sha256=454cfb3387dd964dad59d868a522ba8ebbb1181df78bac6e750133eb5770f96d +mailu-webdav|d5d73d88a2764eaded36de6cbc7fab3fd21956df393d4cf9fecc58217a80e3b6|ghcr.io/mailu/radicale:2024.06|sha256:890973bd7de583a7c8cb2c2bbc939249e007418066b69f59cd0c6903b7f35dfc|running|healthy|2026-07-10T15:30:56.620268969Z|arrmailnetmail_radicale|always|arrmailnetmail|healthcheck_sha256=0eaa30f40ef1bb32694b25d91598460713955379cbe30e51af90204e6c29aeff +makearmy-app|3c10dda3815a2c0b0c494fb99a198a331b85c61f0b27ed1537bd2743355fd363|lasereverythingnetdb-makearmy-app|sha256:6c8190861686d9bec6f50d705c681d657fbc979c2a8743cf1614d01db0b5d1c6|running|no-healthcheck|2026-07-10T16:30:29.09880902Z|makearmy_net|unless-stopped|lasereverythingnetdb|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jibri-1|d9799783148b365cb04f3eeb4f52217cc32e377b47fb69919d6ff18f05430d1d|jitsi/jibri:stable|sha256:dfe4d14ee76e0b054c226b27976d43b0c4ed6fa9774b8acd06b0c4f391dbbb95|running|no-healthcheck|2026-07-02T16:21:43.11777777Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jicofo-1|9f10baac0b699ef704ffbceab5d182a769f45f964a08fff13984b67f421370ac|jitsi/jicofo:stable|sha256:80619f3a1c08c4bb6ccc44af0a0829a1bc817904a5819da2762bca0ea7d840fe|running|no-healthcheck|2026-07-02T16:21:42.695779854Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jvb-1|dcb946a8243ca5b8b3307c9e0e47474e24bd8a0b0f738e5558ef1e084cd0a26d|jitsi/jvb:stable|sha256:5a7999e458cc16a79a7b1082de760d5240d938ea2fd1c74dfc1cb7c32267a2d2|running|no-healthcheck|2026-07-02T16:21:42.696967273Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-prosody-1|7e55738c8ef8fa72966f30ee08e08f78ef9d2ce0778f102810e7a1e9a65ee427|jitsi/prosody:stable|sha256:4ecc8a3ed20b56d628e0980e1bd279a5201a1d749a0d7529cc320a1e57dd13c0|running|no-healthcheck|2026-07-02T16:21:42.57107622Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-web-1|9fcb631e7509b43c6cb8589ba5b9b86a33355fb21f6b0aded53fe249c317ac8a|jitsi/web:stable|sha256:dc69cbf912d43bbd2e6bff921c32f968d0fd20d299f4a6ace213411b3ace77d7|running|no-healthcheck|2026-07-07T23:05:03.005510996Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mastodon-es|8173cfe45df20a1f250fdb1deeaf7b195f9b10048827c3c7f0f6bed3065d5a8c|docker.elastic.co/elasticsearch/elasticsearch:7.10.2|sha256:0b58e1cea50068776b05911440c0eb2865ef64f46704a74ebcf0f8f9d9edff08|running|healthy|2026-07-02T16:21:41.658284982Z|mastodon_net|always|makearmyiomastodon|healthcheck_sha256=43ed0ec1b037c1138b428561ed2f275ae40dbf871887fff30170841101c96871 +mastodon-redis|42a18b0302bab7268fb6e4b37fdb00e7e33e8ab873d29a51935960dfa4bd2c9e|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:41.657158258Z|mastodon_net|always|makearmyiomastodon|healthcheck_sha256=6971523e64ad7ed1de29b647f359cf830fb227fe65b6e7d687b694e79b4cd00a +mastodon-sidekiq|8f854b45b737bc4e36e74ef5f6698decbf36d3d77b837fa30b736d3ec22ede74|ghcr.io/mastodon/mastodon:v4.3.7|sha256:9f42cca562554c9a4b3c39fecf3590aaf48cd4f566054f94135bc7e417b9e609|running|healthy|2026-07-02T16:21:41.771788456Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=63b22b698c07de250a134ce31a15f3b269ad63c4f8fa4896bb920941b5a20e56 +mastodon-streaming|58e667e71825fdf2674c125bf60ed8a1edf075b3dd01a9aeaa69e79c07edd1cf|ghcr.io/mastodon/mastodon-streaming:v4.3.7|sha256:ce444902e7c4152b82ad2526fbd32d0eaa47cc9d6b2a7e4198cd2395579e63cf|running|healthy|2026-07-02T16:21:41.770290748Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=d5da8b454dac278280b9e3931e305a9094a1e446721f9e01e89f3de89772a681 +mastodon-web|355cfac0741d25cee7c18729d4ec0f74355a20edca54ca608102be82bfc90a05|ghcr.io/mastodon/mastodon:v4.3.7|sha256:9f42cca562554c9a4b3c39fecf3590aaf48cd4f566054f94135bc7e417b9e609|running|no-healthcheck|2026-07-02T16:21:41.790863717Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +misskey-redis|ee4b788482e8fdaf635f5bb4e66ccf757f8b8e96d85175efc56e53f09d5550e3|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:22.84032892Z|misskey_net|unless-stopped|makearmyiodeck|healthcheck_sha256=6a1d3a9edb36c3a4ce5fcbd09886f5e885dcfb752bcece4036250d3ded7a95b0 +misskey|fafd13e69d38892a3e1cdb33ebe709b2de1a13807a7db037035bad815d2c4e59|misskey/misskey:latest|sha256:368867af5f043346679e8721eb4560f2633cfe1a889fcf0422b6e845b7fae5b6|running|healthy|2026-07-02T16:21:28.456308969Z|backend_net|unless-stopped|makearmyiodeck|healthcheck_sha256=5ccbecc9fc9854d3822bad70f7615ca2b2189f2ae0376a276e1a1cd785d27328 +mysql_shared|37e9091b4f9db9c4434b664903283248fe9723a9717ba3a407141d625c6ab272|mariadb:10.11|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|running|no-healthcheck|2026-07-02T16:21:19.526467097Z|backend_net|unless-stopped|database|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +navidrome|0d66d26de7b4dc30c3f2629d8db37a68b45dec6b69805aac1d48682999dc8bd4|deluan/navidrome:latest|sha256:6567e3810d544da3bb067f50aba7b05b3b029acacd7b17654f2310eee21756c8|running|no-healthcheck|2026-07-02T16:22:15.324814438Z|backend_net|unless-stopped|s6vinetmusic|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-app|da4c05c513a3fd41b3816f4e3b267489bc795e07af0411d69e54a8fd4c303341|nextcloud:fpm|sha256:192578f11d97117460f29c0c42e4e3676200f6e0da97f263f34d59dfbfd9b98b|running|no-healthcheck|2026-07-02T16:21:22.194816111Z|arrmail_backend|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-redis|8a22ef42d843c6af32706995bdac253c66b2dbdfc05ad63d154e4f6130149dc4|redis:alpine|sha256:55563f45704d59e891b6aa643014bb6b562abf21909a44b3b297502415b68e29|running|no-healthcheck|2026-07-02T16:21:22.079098096Z|nextcloud_ma_net|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-web|47788388748bf09d5a90f9e04078d1f5387a1e55e8dab562d1356568684851e6|nginx:alpine|sha256:ea51152ef8c480b999cee0271a288c698704b18483276119b1253e576ef3232f|running|no-healthcheck|2026-07-02T16:21:22.386376738Z|nextcloud_ma_net|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nzbhydra2|72577626a7b78f5e421a3aa2170ca0779d1ae32833b8ab06af5e15f6c21b54b7|lscr.io/linuxserver/nzbhydra2|sha256:de294ca9aa92795dca1f3e18c5bd1f23a6f88f580ed35537c91910e898d319b9|running|no-healthcheck|2026-07-02T16:22:04.638532863Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +owncast|f49b181fbd7098f6794f49dedb85d7f0f0b1b96302571d328d5c53c6c53477ca|owncast/owncast:latest|sha256:e6eabb9b993941043e9a6feb8f3d94d54854afc1d268310cfedc4a4a45f9b31b|running|no-healthcheck|2026-07-02T16:21:21.767199243Z|makearmyiocast_default|unless-stopped|makearmyiocast|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +peertube-redis|af5cd4f35df32636f42dbe64fb8d6f76ddfa4f0c0c805a33e8a5ab80b14de0f2|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|no-healthcheck|2026-07-02T16:21:45.580356479Z|makearmyiowatch_peertube_net|unless-stopped|makearmyiowatch|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +peertube|f1f9b4a119fe5b3b4595ae065743c22340bd49d506c8e1dec91396177d02edb4|chocobozzz/peertube:production-bookworm|sha256:95cafee12c7ba26c7427369020a5fe47f0ca4f2c0a9125038a678abda7a69c9b|running|no-healthcheck|2026-07-02T16:21:45.796042043Z|backend_net|unless-stopped|makearmyiowatch|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +picsur-redis|b209f19682835276e80e17d0ac3f1e036d6b1fda8756dcac68fc122d0c4fa3f8|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:29.04863531Z|picsur_net|unless-stopped|makearmyioimages|healthcheck_sha256=147968879ed0db0afaec1547f804e197ae69ccc63932fd3abbe258ab467010d3 +picsur|64562af622743c5e95fed7cd0e400fcb754e67d524f3692308ae11420dc0e0e6|ghcr.io/caramelfur/picsur:latest|sha256:1807c0cf959b86dddbf3a92effb9d8cde12b3412c0a399bfd28720fdc37238c4|running|healthy|2026-07-02T16:21:39.647252782Z|backend_net|unless-stopped|makearmyioimages|healthcheck_sha256=ec000ecdddc3d5596f4bc9628961c417aa7b8c1a3b5d2df4c98640ad362f5dd5 +pinchflat|85fe3664962fcfa588aa37133ff74181f16806d98ed22b9a3d921d266698389c|keglin/pinchflat:latest|sha256:753a348c4189cab415077a0ba39e429f3a2d827ca9944482fba39e89d7670359|running|healthy|2026-07-02T16:22:17.898598212Z|s6vinetyt_default|unless-stopped|s6vinetyt|healthcheck_sha256=135a29148f28914de8f172b817ecb24ba481550c1b7266a8db237818fd6b8455 +pixelfed-ma-redis|2f5e7667382929d159967f793544f3d797d1bc081b00d8e2e8e569c12f34efc6|redis:6|sha256:2acfd509b0322ec1849e7e8cae7acfed5abfa449aef850d960669fb93e31a0da|running|no-healthcheck|2026-07-02T16:21:43.915226725Z|pixelfed_ma_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +pixelfed-ma-web|4bf3656aa29bb29bc731b8a31e22f0b3c7e26b04439d400b2f7b92a43d131c75|elestio/pixelfed:latest|sha256:96f2928fac9954a5926816c08b0828f0aac91101ed44f35e45870af0b07ca4b3|running|no-healthcheck|2026-07-02T16:21:44.127434453Z|backend_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +pixelfed-ma-worker|4533b9ed4e4d02ee916e99ef1d042fb11f6cb140168748fe15a61112a2f27794|elestio/pixelfed:latest|sha256:96f2928fac9954a5926816c08b0828f0aac91101ed44f35e45870af0b07ca4b3|running|no-healthcheck|2026-07-02T16:21:44.134015364Z|backend_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +postgres_shared|16a46598e773446ef94853d20ca2aa4f636599c4fabb5dc945038891e7327f75|tensorchord/pgvecto-rs:pg16-v0.3.0|sha256:edaefebbe512002c6bc57af37cfffb53d70ede0c4f213dd9344a0cd42276cee5|running|no-healthcheck|2026-07-02T16:21:19.527512266Z|backend_net|unless-stopped|database|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +privatebin|908728b26d94527c44fa334bd0e506b478d6ab143efa292ce7444ebc2d15e8c8|privatebin/nginx-fpm-alpine|sha256:1cd5a4d4a4d712cd3cc5b3997c47211279e2180a578ab2114285a5a8ad38117a|running|no-healthcheck|2026-07-02T16:21:43.509343441Z|privatebin_rw_net|unless-stopped|makearmyiopaste|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +prowlarr|89d2cb4ae3df4b62d58914957f583650d9b21f90830f120d09fc70c7e901c681|linuxserver/prowlarr|sha256:e21be23a845221afc1f4a650b864c294758cde345ba7c865f01e2686274dedb7|running|healthy|2026-07-02T16:22:04.640131595Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=13f99794bea67548cc642314145eec7125eb7f719dada9768aba07a1a476b795 +qbittorrent|af4d51e98c4a6293183474353c87e43e38c26e675fb74048ddc9f9064a62c0ae|lscr.io/linuxserver/qbittorrent:latest|sha256:a5d5cf51c98df7347d8e9ed7bf2c83c4b7f9f0e9a3981410e8499927541cb350|running|no-healthcheck|2026-07-02T16:22:04.635570271Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +radarr|161e0cab15f684eff7237b0237483cf2d25c628f55c9a78b07d917054ba0d4da|linuxserver/radarr|sha256:ecf70f15bca59f11387be96af3d2eea3a733d8f16946e763923185d4d297fa08|running|healthy|2026-07-02T16:22:04.631684293Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=f71b83e7755a8b028db89f814a6a838ce7119af31ce52ce7e8ba8976096250c1 +recyclarr|55cd226a6d1cc7b0375ab72aa7c23a17ed1ecbb1ff7e5790701589a777c6d0cd|ghcr.io/recyclarr/recyclarr|sha256:c57a7e2fe3090e3f43a0dd6686cde6c01db71c8d4a85fcb3b789d0ac473d874c|running|no-healthcheck|2026-07-02T16:22:05.297908155Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +romm-db|32e6a3a86bd8a241fba5a3ee4bd81f8e40d5d00988b6f7b4382d33af10a301fe|mariadb:latest|sha256:ca20b0713cc87f0e9a75d154f677e3f9469e1281f21dd68dc551bf3d93467e27|running|healthy|2026-07-02T16:21:47.839091367Z|s6vinetgames_default|unless-stopped|s6vinetgames|healthcheck_sha256=d5f5eabd7fcdf68fb96e631355d683a153faa27d31ac2c96ae5f4263929c436e +romm|52eadbc4e6fedce3487bc0ccc9b5f1e0fe631349d3246ce95430dab75d10ae31|rommapp/romm:latest|sha256:0ec219d3cf24a9ee76f36c318ec03fca842d474b9ba47a6484e40b62b11983dc|running|no-healthcheck|2026-07-02T16:21:58.569925827Z|s6vinetgames_default|unless-stopped|s6vinetgames|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +sabnzbd|23e190c5e003059a5b331976ad6bc5773a7089c4c52b967933332d705e8be4cd|lscr.io/linuxserver/sabnzbd|sha256:8a22dade7777d4c9c29922881dd849ffe8059be3b7b2a97fc137a745057f9894|running|no-healthcheck|2026-07-02T16:22:04.634220659Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +slskd-portwatch|5be4dca461964ba4a401c0c49f69bf4147dfe72e3c318414a768e66684c49235|alpine:3.20|sha256:bf8527eb54c3680e728d5b4b383a8ba730d72dae7236fbc8dff97ed6b224a731|running|no-healthcheck|2026-07-02T16:22:14.634814016Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +slskd|50aebf40411108683ec8fbd2bb81f11a9275894da0bf3b80e32c492ef180c1ef|slskd/slskd:latest|sha256:4037f8009b7a98e3ef10b0b590717895c09779aeed060e093fc3ef2f8b027cf2|running|healthy|2026-07-08T12:00:29.038610186Z|container:f73ac36f101be4afadf1933ffa15f86bb05642d50b6c6859c904aa2a743a6c8b|unless-stopped|s6vinetmedia|healthcheck_sha256=2e42bc8cb3985bfbe1ec2460cd8d9a2fa7b2fd9ebc5e88bc824f8b95e39d6dca +snappymail|79ea0d668d4289c270a148183c3c6465b9e70521953bcadef9de7cca49c20f6a|djmaze/snappymail:latest|sha256:741d1bdf62b2cd4f3ff8c1d58a86bec7ba1aa644fbce45ea275d6972f89ac2bf|running|no-healthcheck|2026-07-10T01:56:51.598114125Z|snappy_net|unless-stopped|arrmailnet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +sonarr|fbd5ba72eaeccc4881818317d6fc81a285fa2a1161989675f53a666ce8c11002|linuxserver/sonarr|sha256:66f3b9a7c27e21a06e3c1a5f8f17b2b00e917291f0aaf07cece29f9fd9a26e4f|running|unhealthy|2026-07-02T16:22:04.632855946Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=3b816cea3c7006fde5b6197792c65249d978e96c07caf3cc61136b553107433e +tdarr|ba0f5cf378fd25e9a6b95cd6b53ea377a4cb86288fe131d7379e5e7e7a24b8fb|ghcr.io/haveagitgat/tdarr:latest|sha256:6805431096bd3fa6e2aa1005dc1f3834545b48f6a03f09243a653e2005a3a808|running|no-healthcheck|2026-07-02T16:21:59.102117444Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +vaultwarden|6633442e948d174acda740c2a26ba13782f1ac57f24839e0ba51791d9fc3b51d|vaultwarden/server:latest|sha256:3e3ce0360c32d2dcc2bbe138f7716014dc3eee45f235e44603e4761e46601007|running|healthy|2026-07-02T16:21:21.085620901Z|vault_net|unless-stopped|arrmailnetpass|healthcheck_sha256=c0ba6b061e69edd02095fa8c73f0065e6df9440345b0b0f53d5dd24e341cc665 +vdo_wss|f40b63fbbd9d80f42dd8477226aa734c88db0009d04d493701f7507de00c2382|node:20-alpine|sha256:11cedc39e663e7c5d5cb9cc77a461a0d2adc25537b94e6831a6108f09cb2001b|running|no-healthcheck|2026-06-30T17:55:34.686303735Z|makearmyiovdo_default|unless-stopped|makearmyiovdo|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +vpn-slskd|f73ac36f101be4afadf1933ffa15f86bb05642d50b6c6859c904aa2a743a6c8b|qmcgaw/gluetun|sha256:5694e33296a1d44a563e83e51113970b230acf2878004f01099ad4a4b83bc762|running|healthy|2026-07-02T16:21:58.912570969Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=8a73e8de0ca0b93f3e1b75f81d2ffe1c320f5ede18b34ec15db87f285f778104 +vpn|ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|qmcgaw/gluetun|sha256:5694e33296a1d44a563e83e51113970b230acf2878004f01099ad4a4b83bc762|running|healthy|2026-07-02T16:21:58.9115723Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=740a01b114720db4fbb6e078898c810bd1ff19f9227cc3a1bd01926f222aea8b +yacht|d2a07818987aff223c090be88530007784385b3352e5f53aa10ec881a305464b|ghcr.io/selfhostedpro/yacht:latest|sha256:d79060100eb7707aae957b78a4406dfb5b0f83d83543f3e132d8bb24da8abc9c|running|no-healthcheck|2026-07-02T16:21:47.517372813Z|yacht_s6_net|unless-stopped|s6vinetdocker|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/directus-stopped.allowlist.psv b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/directus-stopped.allowlist.psv new file mode 100644 index 0000000..a77d8b1 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/directus-stopped.allowlist.psv @@ -0,0 +1 @@ +directus|c65e7651bfb8827b712f3ed57fff5839dc5cdc87f9f416b7a09f389c7864a1a3|directus/directus:latest|sha256:dd5537b4b35a4a980452e8160def97e63bfd0a6b92e6d81ec980a3300801ce2a|exited|no-healthcheck|2026-07-02T16:21:21.312699245Z|backend_net|no|lasereverythingnetforms|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/restart-policy-no.psv b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/restart-policy-no.psv new file mode 100644 index 0000000..1fce713 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/restart-policy-no.psv @@ -0,0 +1,2 @@ +castopod-redis|45094b8a921517979c9a8a519c7ff2fc71a540656f2309ffbd0696cf56bf5dcf|redis:7.2-alpine|sha256:b6636bae9624cba73c69fc9d21318151217a103a1db600b8012db8c460785c26|running|no-healthcheck|2026-07-02T16:21:44.78699982Z|castopod_ma_net|no|makearmyiopodcast|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +castopod|b3612901d76d0b2be752762fb1e5d49ca7b521a648ab9b6f332ff7ace61ba54a|castopod/castopod:latest|sha256:d624bd1d45a4b929b9202cae2c4290cdf2624b6bf172fe2115ce20bdb359a428|running|healthy|2026-07-02T16:21:44.790327595Z|backend_net|no|makearmyiopodcast|healthcheck_sha256=a8def15088dacf1eb21b7a57747b2113566689d9876c5d690d7c3269562ec5cc diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/running-containers.psv b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/running-containers.psv new file mode 100644 index 0000000..6b8ec00 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/running-containers.psv @@ -0,0 +1,87 @@ +bazarr|0c752272254cfad9aeb5a6d981a8d354c0676a18eb14008b4613c7a442ea995c|linuxserver/bazarr|sha256:71677d1a237182abf46f607d346109c5a78aae3973d01b143cd89f39569345fa|running|no-healthcheck|2026-07-02T16:22:04.651521486Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +bgbye|2d7d7a1250b29f5e66c3068a4077e2d3df801de3f0c1802882aae1e39a84b05e|lasereverythingnetdb-bgbye|sha256:fa4994a5386cd97cba309b273025478722b06a8985dae249bb80c50a170355ce|running|healthy|2026-07-02T16:23:13.15908777Z|makearmy_net|unless-stopped|lasereverythingnetdb|healthcheck_sha256=6e5884b66db6e8cc64ccec2fc861dc2ee21b1ffce0b8555fdffd69811f89eb3a +buildx_buildkit_defaultbuilder0|6b0affe65f5e3589baa0d57169713a1d9ffa5df5c9f6484a8e32bc4070d67ee6|moby/buildkit:buildx-stable-1|sha256:fe0990fb85c4586aaa94e599905b75a2676664f065f29bea67ebcd5b2fe88acb|running|no-healthcheck|2026-06-30T17:45:39.545876028Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +castopod-redis|45094b8a921517979c9a8a519c7ff2fc71a540656f2309ffbd0696cf56bf5dcf|redis:7.2-alpine|sha256:b6636bae9624cba73c69fc9d21318151217a103a1db600b8012db8c460785c26|running|no-healthcheck|2026-07-02T16:21:44.78699982Z|castopod_ma_net|no|makearmyiopodcast|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +castopod|b3612901d76d0b2be752762fb1e5d49ca7b521a648ab9b6f332ff7ace61ba54a|castopod/castopod:latest|sha256:d624bd1d45a4b929b9202cae2c4290cdf2624b6bf172fe2115ce20bdb359a428|running|healthy|2026-07-02T16:21:44.790327595Z|backend_net|no|makearmyiopodcast|healthcheck_sha256=a8def15088dacf1eb21b7a57747b2113566689d9876c5d690d7c3269562ec5cc +collabora|bcf6738bab2b22f3db163f88f28608548a748a49e6088842e6e687a968c65371|collabora/code:latest|sha256:bf688dedd8778797a551ef5f768c33993cbecc23b9d68527176a59db5c7726c0|running|no-healthcheck|2026-07-02T16:22:15.786914258Z|collabora_s6_net|always|s6vinetoffice|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +continuwuity|a22c7cef0512cefc66441cc99d2b6f4ce36c48e70177c04dfdf88a44e91396ad|forgejo.ellis.link/continuwuation/continuwuity:latest|sha256:f45ecc72598a18e69e16fa7addbb5300b6eba7cd1bf0bf423a8d0b834c8c7abf|running|no-healthcheck|2026-07-02T16:21:42.218988545Z|conduit_net|unless-stopped|makearmyiomatrix|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +dcm|1d3291029cb4f3850e43ea93d6bc3a3ceb1d12ecfea27f136b99c3af61e65d75|ghcr.io/ajnart/dcm|sha256:2a6935c5032df59d0986753a05250dacfd66522554471fd54b0d302a67d6de98|running|no-healthcheck|2026-07-02T16:21:22.600579243Z|makearmyiodcm_default|unless-stopped|makearmyiodcm|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +ergo|f0d41caf4e9ec55b0bf677d8ba622660e918c30094b643cdf09926df545ca4cb|ghcr.io/ergochat/ergo:stable|sha256:ca8d10822089ef9fd05ba5cc18049b687bc46e2e0cc5901aed4bf92da337a7a0|running|no-healthcheck|2026-07-09T17:20:10.183703547Z|irc_net|on-failure|makearmyioirc|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +flaresolverr|3ce787aacca3e92e836526c81c7e6fab2dcb7d810b04fbf521e5deba2fd3039f|ghcr.io/flaresolverr/flaresolverr|sha256:894893ed4d4557764df3e74d27c5bc1b3944e5a33ed410e62ab7c744b348f415|running|healthy|2026-07-02T16:22:04.654391107Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=5b79b0ceb45d2b3ea48a3a1ab49a3c8a7ab3d42b98e63a185986126b8f6e5b71 +forgejo|1befcad10bf9b56880c9e2b223e2dd6e0c5dd500d1453a83768e974ced54273d|codeberg.org/forgejo/forgejo:11.0.1|sha256:49388d84630e37520e7c5d4971dd94564a7a39f18f66b60d40c32b66f7ff40b9|running|no-healthcheck|2026-07-10T00:57:13.158697179Z|backend_net|unless-stopped|makearmyioforge|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +heimdall|c8bab4edf73f1265ab91dd384d8942562827f8ce2185fb65e398c8e923872e13|lscr.io/linuxserver/heimdall:latest|sha256:96a3814546b9465dbee1cce1c9547bed5b6f1a13ca751f239cf88502f396df67|running|no-healthcheck|2026-07-02T16:21:46.899952815Z|heimdall_s6_net|unless-stopped|s6vinet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +immich_machine_learning|2b6046557c314400dec23f1caae69eae07a8e07542db79957f7b7e86d41aaff6|ghcr.io/immich-app/immich-machine-learning:release|sha256:088b410d324842faeeeb69d0d5fef6b3eccee3f6fe3c68a2b1235649a86872c4|running|healthy|2026-07-02T16:22:16.356336193Z|backend_net|always|immich|healthcheck_sha256=06fc5259e91d939c46d4cf740394a5057ea58131e5df6789d7e4f5ebbb6d7a59 +immich_postgres|17889bb81cea5af9cf6ac6bafe6eba61aeb8d50cb2c0996c699560182796945e|docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52|sha256:3062a7ddd658eee20fcc428853dd34c23c7ad95fb48e3181922ffef11217389a|running|healthy|2026-07-02T16:22:16.362765814Z|immich_s6_net|always|immich|healthcheck_sha256=3b85ca1ca42325d6e6d9501e2adcf04a7a80dcb42159b69d2527d6432473ff32 +immich_redis|a2d1a7bc973927e404a2576cb3de755a890f7ded3f3361a8a065eab88a0d481c|docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8|sha256:6dd588768b9ba229530de57c108536a8d6b98d53d83f217f59167981fda443ca|running|healthy|2026-07-02T16:22:16.364329237Z|immich_s6_net|always|immich|healthcheck_sha256=bd98ba333892e944c8245cd358b2946469adf44d3d8ec7cabb936197e4362807 +immich_server|2115244bab14fec49130c4a3949dfc2773871fc54928f88be963388e5e04d790|ghcr.io/immich-app/immich-server:release|sha256:de80bec4fc193605671b0f9600fe6b20f598fd2cb503ffe1a6e67f7cfd0e5233|running|healthy|2026-07-02T16:22:16.558556945Z|backend_net|always|immich|healthcheck_sha256=b7dce503cff869bbffb63034400837e1ad79981eed7722359480a50dbc96436f +jellyfin|ba3ef59b346479f17f4c2b9fd33828db2638801e408fca115e5289b371f89eaf|jellyfin/jellyfin:latest|sha256:d57d4a0c18b3e51b913bd2f8f0f122eb7c7150ea37bb0e352df9cb329de11d24|running|healthy|2026-07-02T16:22:17.374948578Z|jellyfin_s6_net|unless-stopped|s6vinetwatch|healthcheck_sha256=a22856b3a5dfad87ecc898383d5b49daa6b4b98bca2797ea36cb422e824de47c +jellyseerr|c00ff6cc7213669b85bc8e28a51ffc16779d2489c7378f3911735b8d983dcd05|fallenbagel/jellyseerr|sha256:2742757d9c41bcb4acb76c86c4ce23a8c54d5dbe93a698c815a9a34bed0b18d0|running|no-healthcheck|2026-07-02T16:22:04.637037538Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +kavita|578e4aa1ceeb4eb61ce094ecc862cdce7d68538a2c3e7b17cb29c3f4798597d4|jvmilazz0/kavita:latest|sha256:12ac333d6fde07c087a135dd23fa5b930a96e2a3fde9cfbb281214f040b4cbba|running|healthy|2026-07-02T16:22:04.630545121Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=256947cdccf855523b654955b26538932c2ee76e01f507ef885f1e885437ddb4 +lasereverything-manager-bus-core|791f2ef47fc1c798318b4136773672dc0f8ff08cd104f075f2f317ca0caf1817|ghcr.io/true-good-craft/tgc-bus-core:latest|sha256:e053cb452820daba308e86f456ba7d1e2b0306f058eae2c91ee7122a8634f4bb|running|healthy|2026-07-02T16:21:21.581810214Z|lasereverythingnetmanager_default|unless-stopped|lasereverythingnetmanager|healthcheck_sha256=5f70654d32c1e7e9060a6069ae4e3e8d1b94c196cd73a66ae40c93098344ec59 +lasereverythingnettv-radio-1|297e17de84c4d3c31f9cdeda9dcbefe185e6ec6c48c44c7fdae9f3e3f5ce982c|jrottenberg/ffmpeg:latest|sha256:1cf434af29091eba2d80108e28864540dc98bd3e4e5cda2b497e758d60bacba1|running|no-healthcheck|2026-06-30T17:48:55.163034213Z|lasereverythingnettv_default|unless-stopped|lasereverythingnettv|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lasereverythingnettv-tv-1|776624328baf74b4196474c0962d2e01b1caa21e8bc3a2f6c77aece135faf30e|jrottenberg/ffmpeg:latest|sha256:1cf434af29091eba2d80108e28864540dc98bd3e4e5cda2b497e758d60bacba1|running|no-healthcheck|2026-06-30T17:48:55.161961872Z|lasereverythingnettv_default|unless-stopped|lasereverythingnettv|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +le-directus-source|6adbd503febfd8a6236fc7c941df7bfc748e76dcd510c11e69a3ff02776ff344|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|running|no-healthcheck|2026-07-10T23:15:59.628507071Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +le-payload-pg|2ee8f491397b5cbab595121f0e5e57fa5778329090d27a80b002bdd83a5ee8f2|sha256:639c86b6cacfdf788c075ffb5879286aa0e905965a4832ba6c76ccb0bca9417b|sha256:639c86b6cacfdf788c075ffb5879286aa0e905965a4832ba6c76ccb0bca9417b|running|no-healthcheck|2026-07-10T23:20:42.20954498Z|bridge|unless-stopped||healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-pictrs|22597c5b8ac30d2d715bd538ee15e27e07d8677c9a191c2af741c57b1adbe08d|asonix/pictrs:0.5.17-pre.9|sha256:9a5411f20149156b2bbbcc69b63918e9259d8d4653244eae7482e71177e1b15a|running|no-healthcheck|2026-07-02T16:21:41.143484739Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-proxy|f267f47868f6bd4c0f2b3594c1b23909862fbe853491cd514e9f8205be3499d2|nginx:1-alpine|sha256:ea51152ef8c480b999cee0271a288c698704b18483276119b1253e576ef3232f|running|no-healthcheck|2026-07-02T16:21:41.4297012Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +lemmy-ui|e53db215d6385a424365263fbce8edd9d08f1ea0927bc0aa136af4694ba22d89|dessalines/lemmy-ui:0.19.12|sha256:0a85b3192f29a5b14d41c265d9d5cab60bd281b4a5b5db8f3a85d5d9bf01aaaa|running|healthy|2026-07-02T16:21:41.334389805Z|lemmy_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=fe44578c9a0ef5c3150b372caa763f18afad8938c28752af3570a87d8deb88e4 +lemmy|1f032d67308df68711b5d432398ff31094da65dbdddbb966e60d745390fd0a0d|dessalines/lemmy:0.19.12|sha256:68c00173ade225585ea7d54ed2688f8f23d7d2886ae3f5b4eefd70dbc05b044a|running|no-healthcheck|2026-07-02T16:21:41.230573992Z|backend_net|unless-stopped|makearmyiolemmy|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mailu-admin|ed54ee705edfd3867b98548c7a5054ba8e58eb8fc3f7afbf3e0d5378fe508f4e|ghcr.io/mailu/admin:2024.06|sha256:e4319dc05cd8ee8204ebb0343685d52a9c645f192f92a9ba24e05a702a722894|running|healthy|2026-07-10T15:30:56.858313278Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=65c193d7ce37216310a9ad7324433501d4532a0e95241ba91a6643556dcb2b8d +mailu-antispam|0f2bc2ca4ee1971e3dfdd9534ffa5ffa6bb5b1530f8c1e8c595a7c1303d70cba|ghcr.io/mailu/rspamd:2024.06|sha256:a8619d7f452f04d806b1f334e3e28e123c5f568080e168051fe98c7b3637925d|running|healthy|2026-07-10T15:30:57.245953555Z|arrmailnetmail_clamav|always|arrmailnetmail|healthcheck_sha256=7ffc0eff154ba7ec4712f405258f56347f428b7f2ec716554a9f6c1fed3b896d +mailu-antivirus|22b05b37de3374f615e15e79cee3632e3f2398a2c50a5183a3baa94add519b62|clamav/clamav-debian:1.4|sha256:f8a1f4389e5cbfb43ffc839d751bf746c6c27f5de780ba1fcde15d596f94287b|running|healthy|2026-07-10T15:30:56.621336608Z|arrmailnetmail_clamav|always|arrmailnetmail|healthcheck_sha256=713e2ecbe297d1e71fa7f1819859d65ad738041705100f5c4ae73f4ae551fb60 +mailu-fetchmail|a47a586ae5cdb3d810932c4dfe06ec242ec02cc3999f7ecec2fac1779cf14ffc|ghcr.io/mailu/fetchmail:2024.06|sha256:eccbdf53fb66dc34a49bde349f53eb9d1bba60c98a1be5d9eabe68a726f13e6a|running|healthy|2026-07-10T15:30:57.460051545Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=001e1e50755b8a7ad162dfd92611a0d0944aa07d8ca6dc19ebffb0c60494878e +mailu-front|a82753aefc6c8243c7c71caa0ef289a1a2e83c3fb1f9e93e779e56267353d11c|ghcr.io/mailu/nginx:2024.06|sha256:30a4f996f255fe498ed847b9a3759efd45fc5d8bd5c4dc121896e83615e60b0d|running|healthy|2026-07-10T15:30:56.726639815Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=c379c13a800f5417c0692896be411b09f7fe5a95d530f676abfd7d2dae1ea24c +mailu-imap|549c48f83f429fd9b1e52f7752012b1250107dc511c1b7ee94ac2a49b521a726|ghcr.io/mailu/dovecot:2024.06|sha256:27aceac0272f97361a5a5b1d5abea7099e10f409e3d28a6096f9111fe90bf347|running|healthy|2026-07-10T15:30:57.247440245Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=9462f6ccccab8ea58f34afb7cfaae178b2d36f27a29e903b89ea4a7a1ed73cd6 +mailu-oletools|841c57f99cfeec974e757666484b8c1a37ca0f2696d719d49317294253edb33b|ghcr.io/mailu/oletools:2024.06|sha256:76fb07165f54411c1136a449ba2963fe11618f0cf4629fdc13eadd628d3528c9|running|healthy|2026-07-10T15:30:56.729115113Z|arrmailnetmail_oletools|always|arrmailnetmail|healthcheck_sha256=b1b33aade32e78a9484d43059f9f651eef8a2e8b8910893a4436f15a078abd08 +mailu-redis|4f1b06b40d4ebe43ee8391c91d0c02645a7f23dc91229123a0a25e9098eb567d|redis:alpine|sha256:55563f45704d59e891b6aa643014bb6b562abf21909a44b3b297502415b68e29|running|no-healthcheck|2026-07-10T15:30:56.720208042Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mailu-resolver|96d3aaea44a0a8779c2a11fd7cb341bbc9aa23ad879381c5f7a247a0554cc444|ghcr.io/mailu/unbound:2024.06|sha256:d30593c8bf42bfd1c46bd1ac7ee90862b0cfa7e96fd10da1dacc2c69cafb443d|running|healthy|2026-07-10T15:30:56.617962677Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=5b9b365275d40b354506f2b02070d7785700a374ee09de8d7ee8527bbbf1b16f +mailu-smtp|22e3c459d26b808ffca118df679aae2c9cc511de3fcaba4a9847770df6f24de5|ghcr.io/mailu/postfix:2024.06|sha256:0bed674f22e609e0b2f6780865097a41ed6ca5307db4bcc0794a35e2e7e976c9|running|healthy|2026-07-10T15:30:57.241512918Z|mailu_net|always|arrmailnetmail|healthcheck_sha256=9b234890e871375cec386d4edacf17bcbd8a5504fab8b1177d956c56622a08aa +mailu-tika|60ba021f422056617055244f608c881f5ce282c2d1590e52d8d2719a44b1eb98|apache/tika:2.9.2.1-full|sha256:e646191162f8cb7aef0b512d54c028f62bd86e0e5f53b314ab82cfac7ef292d7|running|healthy|2026-07-10T15:30:56.723391978Z|arrmailnetmail_tika|always|arrmailnetmail|healthcheck_sha256=454cfb3387dd964dad59d868a522ba8ebbb1181df78bac6e750133eb5770f96d +mailu-webdav|d5d73d88a2764eaded36de6cbc7fab3fd21956df393d4cf9fecc58217a80e3b6|ghcr.io/mailu/radicale:2024.06|sha256:890973bd7de583a7c8cb2c2bbc939249e007418066b69f59cd0c6903b7f35dfc|running|healthy|2026-07-10T15:30:56.620268969Z|arrmailnetmail_radicale|always|arrmailnetmail|healthcheck_sha256=0eaa30f40ef1bb32694b25d91598460713955379cbe30e51af90204e6c29aeff +makearmy-app|3c10dda3815a2c0b0c494fb99a198a331b85c61f0b27ed1537bd2743355fd363|lasereverythingnetdb-makearmy-app|sha256:6c8190861686d9bec6f50d705c681d657fbc979c2a8743cf1614d01db0b5d1c6|running|no-healthcheck|2026-07-10T16:30:29.09880902Z|makearmy_net|unless-stopped|lasereverythingnetdb|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jibri-1|d9799783148b365cb04f3eeb4f52217cc32e377b47fb69919d6ff18f05430d1d|jitsi/jibri:stable|sha256:dfe4d14ee76e0b054c226b27976d43b0c4ed6fa9774b8acd06b0c4f391dbbb95|running|no-healthcheck|2026-07-02T16:21:43.11777777Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jicofo-1|9f10baac0b699ef704ffbceab5d182a769f45f964a08fff13984b67f421370ac|jitsi/jicofo:stable|sha256:80619f3a1c08c4bb6ccc44af0a0829a1bc817904a5819da2762bca0ea7d840fe|running|no-healthcheck|2026-07-02T16:21:42.695779854Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-jvb-1|dcb946a8243ca5b8b3307c9e0e47474e24bd8a0b0f738e5558ef1e084cd0a26d|jitsi/jvb:stable|sha256:5a7999e458cc16a79a7b1082de760d5240d938ea2fd1c74dfc1cb7c32267a2d2|running|no-healthcheck|2026-07-02T16:21:42.696967273Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-prosody-1|7e55738c8ef8fa72966f30ee08e08f78ef9d2ce0778f102810e7a1e9a65ee427|jitsi/prosody:stable|sha256:4ecc8a3ed20b56d628e0980e1bd279a5201a1d749a0d7529cc320a1e57dd13c0|running|no-healthcheck|2026-07-02T16:21:42.57107622Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +makearmyiomeet-web-1|9fcb631e7509b43c6cb8589ba5b9b86a33355fb21f6b0aded53fe249c317ac8a|jitsi/web:stable|sha256:dc69cbf912d43bbd2e6bff921c32f968d0fd20d299f4a6ace213411b3ace77d7|running|no-healthcheck|2026-07-07T23:05:03.005510996Z|meet_net|unless-stopped|makearmyiomeet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +mastodon-es|8173cfe45df20a1f250fdb1deeaf7b195f9b10048827c3c7f0f6bed3065d5a8c|docker.elastic.co/elasticsearch/elasticsearch:7.10.2|sha256:0b58e1cea50068776b05911440c0eb2865ef64f46704a74ebcf0f8f9d9edff08|running|healthy|2026-07-02T16:21:41.658284982Z|mastodon_net|always|makearmyiomastodon|healthcheck_sha256=43ed0ec1b037c1138b428561ed2f275ae40dbf871887fff30170841101c96871 +mastodon-redis|42a18b0302bab7268fb6e4b37fdb00e7e33e8ab873d29a51935960dfa4bd2c9e|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:41.657158258Z|mastodon_net|always|makearmyiomastodon|healthcheck_sha256=6971523e64ad7ed1de29b647f359cf830fb227fe65b6e7d687b694e79b4cd00a +mastodon-sidekiq|8f854b45b737bc4e36e74ef5f6698decbf36d3d77b837fa30b736d3ec22ede74|ghcr.io/mastodon/mastodon:v4.3.7|sha256:9f42cca562554c9a4b3c39fecf3590aaf48cd4f566054f94135bc7e417b9e609|running|healthy|2026-07-02T16:21:41.771788456Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=63b22b698c07de250a134ce31a15f3b269ad63c4f8fa4896bb920941b5a20e56 +mastodon-streaming|58e667e71825fdf2674c125bf60ed8a1edf075b3dd01a9aeaa69e79c07edd1cf|ghcr.io/mastodon/mastodon-streaming:v4.3.7|sha256:ce444902e7c4152b82ad2526fbd32d0eaa47cc9d6b2a7e4198cd2395579e63cf|running|healthy|2026-07-02T16:21:41.770290748Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=d5da8b454dac278280b9e3931e305a9094a1e446721f9e01e89f3de89772a681 +mastodon-web|355cfac0741d25cee7c18729d4ec0f74355a20edca54ca608102be82bfc90a05|ghcr.io/mastodon/mastodon:v4.3.7|sha256:9f42cca562554c9a4b3c39fecf3590aaf48cd4f566054f94135bc7e417b9e609|running|no-healthcheck|2026-07-02T16:21:41.790863717Z|backend_net|always|makearmyiomastodon|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +misskey-redis|ee4b788482e8fdaf635f5bb4e66ccf757f8b8e96d85175efc56e53f09d5550e3|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:22.84032892Z|misskey_net|unless-stopped|makearmyiodeck|healthcheck_sha256=6a1d3a9edb36c3a4ce5fcbd09886f5e885dcfb752bcece4036250d3ded7a95b0 +misskey|fafd13e69d38892a3e1cdb33ebe709b2de1a13807a7db037035bad815d2c4e59|misskey/misskey:latest|sha256:368867af5f043346679e8721eb4560f2633cfe1a889fcf0422b6e845b7fae5b6|running|healthy|2026-07-02T16:21:28.456308969Z|backend_net|unless-stopped|makearmyiodeck|healthcheck_sha256=5ccbecc9fc9854d3822bad70f7615ca2b2189f2ae0376a276e1a1cd785d27328 +mysql_shared|37e9091b4f9db9c4434b664903283248fe9723a9717ba3a407141d625c6ab272|mariadb:10.11|sha256:72311f2ba18f5530a667b9a806bb1720a8c9d95de6b20dd085a808901eb614b8|running|no-healthcheck|2026-07-02T16:21:19.526467097Z|backend_net|unless-stopped|database|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +navidrome|0d66d26de7b4dc30c3f2629d8db37a68b45dec6b69805aac1d48682999dc8bd4|deluan/navidrome:latest|sha256:6567e3810d544da3bb067f50aba7b05b3b029acacd7b17654f2310eee21756c8|running|no-healthcheck|2026-07-02T16:22:15.324814438Z|backend_net|unless-stopped|s6vinetmusic|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-app|da4c05c513a3fd41b3816f4e3b267489bc795e07af0411d69e54a8fd4c303341|nextcloud:fpm|sha256:192578f11d97117460f29c0c42e4e3676200f6e0da97f263f34d59dfbfd9b98b|running|no-healthcheck|2026-07-02T16:21:22.194816111Z|arrmail_backend|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-redis|8a22ef42d843c6af32706995bdac253c66b2dbdfc05ad63d154e4f6130149dc4|redis:alpine|sha256:55563f45704d59e891b6aa643014bb6b562abf21909a44b3b297502415b68e29|running|no-healthcheck|2026-07-02T16:21:22.079098096Z|nextcloud_ma_net|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nextcloud-ma-web|47788388748bf09d5a90f9e04078d1f5387a1e55e8dab562d1356568684851e6|nginx:alpine|sha256:ea51152ef8c480b999cee0271a288c698704b18483276119b1253e576ef3232f|running|no-healthcheck|2026-07-02T16:21:22.386376738Z|nextcloud_ma_net|always|makearmyiocloud|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +nzbhydra2|72577626a7b78f5e421a3aa2170ca0779d1ae32833b8ab06af5e15f6c21b54b7|lscr.io/linuxserver/nzbhydra2|sha256:de294ca9aa92795dca1f3e18c5bd1f23a6f88f580ed35537c91910e898d319b9|running|no-healthcheck|2026-07-02T16:22:04.638532863Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +owncast|f49b181fbd7098f6794f49dedb85d7f0f0b1b96302571d328d5c53c6c53477ca|owncast/owncast:latest|sha256:e6eabb9b993941043e9a6feb8f3d94d54854afc1d268310cfedc4a4a45f9b31b|running|no-healthcheck|2026-07-02T16:21:21.767199243Z|makearmyiocast_default|unless-stopped|makearmyiocast|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +peertube-redis|af5cd4f35df32636f42dbe64fb8d6f76ddfa4f0c0c805a33e8a5ab80b14de0f2|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|no-healthcheck|2026-07-02T16:21:45.580356479Z|makearmyiowatch_peertube_net|unless-stopped|makearmyiowatch|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +peertube|f1f9b4a119fe5b3b4595ae065743c22340bd49d506c8e1dec91396177d02edb4|chocobozzz/peertube:production-bookworm|sha256:95cafee12c7ba26c7427369020a5fe47f0ca4f2c0a9125038a678abda7a69c9b|running|no-healthcheck|2026-07-02T16:21:45.796042043Z|backend_net|unless-stopped|makearmyiowatch|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +picsur-redis|b209f19682835276e80e17d0ac3f1e036d6b1fda8756dcac68fc122d0c4fa3f8|redis:7-alpine|sha256:487efc0616382465781b8fdc3d6d1db449e6fd80ae23bf48432a2da6b6929908|running|healthy|2026-07-02T16:21:29.04863531Z|picsur_net|unless-stopped|makearmyioimages|healthcheck_sha256=147968879ed0db0afaec1547f804e197ae69ccc63932fd3abbe258ab467010d3 +picsur|64562af622743c5e95fed7cd0e400fcb754e67d524f3692308ae11420dc0e0e6|ghcr.io/caramelfur/picsur:latest|sha256:1807c0cf959b86dddbf3a92effb9d8cde12b3412c0a399bfd28720fdc37238c4|running|healthy|2026-07-02T16:21:39.647252782Z|backend_net|unless-stopped|makearmyioimages|healthcheck_sha256=ec000ecdddc3d5596f4bc9628961c417aa7b8c1a3b5d2df4c98640ad362f5dd5 +pinchflat|85fe3664962fcfa588aa37133ff74181f16806d98ed22b9a3d921d266698389c|keglin/pinchflat:latest|sha256:753a348c4189cab415077a0ba39e429f3a2d827ca9944482fba39e89d7670359|running|healthy|2026-07-02T16:22:17.898598212Z|s6vinetyt_default|unless-stopped|s6vinetyt|healthcheck_sha256=135a29148f28914de8f172b817ecb24ba481550c1b7266a8db237818fd6b8455 +pixelfed-ma-redis|2f5e7667382929d159967f793544f3d797d1bc081b00d8e2e8e569c12f34efc6|redis:6|sha256:2acfd509b0322ec1849e7e8cae7acfed5abfa449aef850d960669fb93e31a0da|running|no-healthcheck|2026-07-02T16:21:43.915226725Z|pixelfed_ma_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +pixelfed-ma-web|4bf3656aa29bb29bc731b8a31e22f0b3c7e26b04439d400b2f7b92a43d131c75|elestio/pixelfed:latest|sha256:96f2928fac9954a5926816c08b0828f0aac91101ed44f35e45870af0b07ca4b3|running|no-healthcheck|2026-07-02T16:21:44.127434453Z|backend_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +pixelfed-ma-worker|4533b9ed4e4d02ee916e99ef1d042fb11f6cb140168748fe15a61112a2f27794|elestio/pixelfed:latest|sha256:96f2928fac9954a5926816c08b0828f0aac91101ed44f35e45870af0b07ca4b3|running|no-healthcheck|2026-07-02T16:21:44.134015364Z|backend_net|unless-stopped|makearmyiopixels|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +postgres_shared|16a46598e773446ef94853d20ca2aa4f636599c4fabb5dc945038891e7327f75|tensorchord/pgvecto-rs:pg16-v0.3.0|sha256:edaefebbe512002c6bc57af37cfffb53d70ede0c4f213dd9344a0cd42276cee5|running|no-healthcheck|2026-07-02T16:21:19.527512266Z|backend_net|unless-stopped|database|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +privatebin|908728b26d94527c44fa334bd0e506b478d6ab143efa292ce7444ebc2d15e8c8|privatebin/nginx-fpm-alpine|sha256:1cd5a4d4a4d712cd3cc5b3997c47211279e2180a578ab2114285a5a8ad38117a|running|no-healthcheck|2026-07-02T16:21:43.509343441Z|privatebin_rw_net|unless-stopped|makearmyiopaste|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +prowlarr|89d2cb4ae3df4b62d58914957f583650d9b21f90830f120d09fc70c7e901c681|linuxserver/prowlarr|sha256:e21be23a845221afc1f4a650b864c294758cde345ba7c865f01e2686274dedb7|running|healthy|2026-07-02T16:22:04.640131595Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=13f99794bea67548cc642314145eec7125eb7f719dada9768aba07a1a476b795 +qbittorrent|af4d51e98c4a6293183474353c87e43e38c26e675fb74048ddc9f9064a62c0ae|lscr.io/linuxserver/qbittorrent:latest|sha256:a5d5cf51c98df7347d8e9ed7bf2c83c4b7f9f0e9a3981410e8499927541cb350|running|no-healthcheck|2026-07-02T16:22:04.635570271Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +radarr|161e0cab15f684eff7237b0237483cf2d25c628f55c9a78b07d917054ba0d4da|linuxserver/radarr|sha256:ecf70f15bca59f11387be96af3d2eea3a733d8f16946e763923185d4d297fa08|running|healthy|2026-07-02T16:22:04.631684293Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=f71b83e7755a8b028db89f814a6a838ce7119af31ce52ce7e8ba8976096250c1 +recyclarr|55cd226a6d1cc7b0375ab72aa7c23a17ed1ecbb1ff7e5790701589a777c6d0cd|ghcr.io/recyclarr/recyclarr|sha256:c57a7e2fe3090e3f43a0dd6686cde6c01db71c8d4a85fcb3b789d0ac473d874c|running|no-healthcheck|2026-07-02T16:22:05.297908155Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +romm-db|32e6a3a86bd8a241fba5a3ee4bd81f8e40d5d00988b6f7b4382d33af10a301fe|mariadb:latest|sha256:ca20b0713cc87f0e9a75d154f677e3f9469e1281f21dd68dc551bf3d93467e27|running|healthy|2026-07-02T16:21:47.839091367Z|s6vinetgames_default|unless-stopped|s6vinetgames|healthcheck_sha256=d5f5eabd7fcdf68fb96e631355d683a153faa27d31ac2c96ae5f4263929c436e +romm|52eadbc4e6fedce3487bc0ccc9b5f1e0fe631349d3246ce95430dab75d10ae31|rommapp/romm:latest|sha256:0ec219d3cf24a9ee76f36c318ec03fca842d474b9ba47a6484e40b62b11983dc|running|no-healthcheck|2026-07-02T16:21:58.569925827Z|s6vinetgames_default|unless-stopped|s6vinetgames|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +sabnzbd|23e190c5e003059a5b331976ad6bc5773a7089c4c52b967933332d705e8be4cd|lscr.io/linuxserver/sabnzbd|sha256:8a22dade7777d4c9c29922881dd849ffe8059be3b7b2a97fc137a745057f9894|running|no-healthcheck|2026-07-02T16:22:04.634220659Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +slskd-portwatch|5be4dca461964ba4a401c0c49f69bf4147dfe72e3c318414a768e66684c49235|alpine:3.20|sha256:bf8527eb54c3680e728d5b4b383a8ba730d72dae7236fbc8dff97ed6b224a731|running|no-healthcheck|2026-07-02T16:22:14.634814016Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +slskd|50aebf40411108683ec8fbd2bb81f11a9275894da0bf3b80e32c492ef180c1ef|slskd/slskd:latest|sha256:4037f8009b7a98e3ef10b0b590717895c09779aeed060e093fc3ef2f8b027cf2|running|healthy|2026-07-08T12:00:29.038610186Z|container:f73ac36f101be4afadf1933ffa15f86bb05642d50b6c6859c904aa2a743a6c8b|unless-stopped|s6vinetmedia|healthcheck_sha256=2e42bc8cb3985bfbe1ec2460cd8d9a2fa7b2fd9ebc5e88bc824f8b95e39d6dca +snappymail|79ea0d668d4289c270a148183c3c6465b9e70521953bcadef9de7cca49c20f6a|djmaze/snappymail:latest|sha256:741d1bdf62b2cd4f3ff8c1d58a86bec7ba1aa644fbce45ea275d6972f89ac2bf|running|no-healthcheck|2026-07-10T01:56:51.598114125Z|snappy_net|unless-stopped|arrmailnet|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +sonarr|fbd5ba72eaeccc4881818317d6fc81a285fa2a1161989675f53a666ce8c11002|linuxserver/sonarr|sha256:66f3b9a7c27e21a06e3c1a5f8f17b2b00e917291f0aaf07cece29f9fd9a26e4f|running|unhealthy|2026-07-02T16:22:04.632855946Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=3b816cea3c7006fde5b6197792c65249d978e96c07caf3cc61136b553107433e +tdarr|ba0f5cf378fd25e9a6b95cd6b53ea377a4cb86288fe131d7379e5e7e7a24b8fb|ghcr.io/haveagitgat/tdarr:latest|sha256:6805431096bd3fa6e2aa1005dc1f3834545b48f6a03f09243a653e2005a3a808|running|no-healthcheck|2026-07-02T16:21:59.102117444Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +vaultwarden|6633442e948d174acda740c2a26ba13782f1ac57f24839e0ba51791d9fc3b51d|vaultwarden/server:latest|sha256:3e3ce0360c32d2dcc2bbe138f7716014dc3eee45f235e44603e4761e46601007|running|healthy|2026-07-02T16:21:21.085620901Z|vault_net|unless-stopped|arrmailnetpass|healthcheck_sha256=c0ba6b061e69edd02095fa8c73f0065e6df9440345b0b0f53d5dd24e341cc665 +vdo_wss|f40b63fbbd9d80f42dd8477226aa734c88db0009d04d493701f7507de00c2382|node:20-alpine|sha256:11cedc39e663e7c5d5cb9cc77a461a0d2adc25537b94e6831a6108f09cb2001b|running|no-healthcheck|2026-06-30T17:55:34.686303735Z|makearmyiovdo_default|unless-stopped|makearmyiovdo|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b +vpn-slskd|f73ac36f101be4afadf1933ffa15f86bb05642d50b6c6859c904aa2a743a6c8b|qmcgaw/gluetun|sha256:5694e33296a1d44a563e83e51113970b230acf2878004f01099ad4a4b83bc762|running|healthy|2026-07-02T16:21:58.912570969Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=8a73e8de0ca0b93f3e1b75f81d2ffe1c320f5ede18b34ec15db87f285f778104 +vpn|ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|qmcgaw/gluetun|sha256:5694e33296a1d44a563e83e51113970b230acf2878004f01099ad4a4b83bc762|running|healthy|2026-07-02T16:21:58.9115723Z|s6vinetmedia_media_s6_net|unless-stopped|s6vinetmedia|healthcheck_sha256=740a01b114720db4fbb6e078898c810bd1ff19f9227cc3a1bd01926f222aea8b +yacht|d2a07818987aff223c090be88530007784385b3352e5f53aa10ec881a305464b|ghcr.io/selfhostedpro/yacht:latest|sha256:d79060100eb7707aae957b78a4406dfb5b0f83d83543f3e132d8bb24da8abc9c|running|no-healthcheck|2026-07-02T16:21:47.517372813Z|yacht_s6_net|unless-stopped|s6vinetdocker|healthcheck_sha256=74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/sonarr-unhealthy.allowlist.psv b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/sonarr-unhealthy.allowlist.psv new file mode 100644 index 0000000..b2b2406 --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/container-disposition/sonarr-unhealthy.allowlist.psv @@ -0,0 +1 @@ +sonarr|fbd5ba72eaeccc4881818317d6fc81a285fa2a1161989675f53a666ce8c11002|linuxserver/sonarr|sha256:66f3b9a7c27e21a06e3c1a5f8f17b2b00e917291f0aaf07cece29f9fd9a26e4f|running|unhealthy|2026-07-02T16:22:04.632855946Z|container:ddb18d46aebae5ce7af0f805ab5222e0825a1a153c71fbed93b8c9a80fc80d89|unless-stopped|s6vinetmedia|healthcheck_sha256=3b816cea3c7006fde5b6197792c65249d978e96c07caf3cc61136b553107433e diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh index 624d2c4..0cf78d2 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/execute-maintenance.sh @@ -92,6 +92,40 @@ validate_health_url_file() { return 0 } +capture_container_fingerprint() { + output=$1 + : > "$output" + docker ps -aq | while IFS= read -r cid; do + [ -n "$cid" ] || continue + base=$(docker inspect --format \ + '{{.Name}}|{{.Id}}|{{.Config.Image}}|{{.Image}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.State.StartedAt}}|{{.HostConfig.NetworkMode}}|{{.HostConfig.RestartPolicy.Name}}|{{index .Config.Labels "com.docker.compose.project"}}' \ + "$cid" 2>/dev/null) + inspect_rc=$? + health_hash=$(docker inspect --format '{{json .Config.Healthcheck}}' "$cid" 2>/dev/null | sha256sum | awk '{print $1}') + hash_rc=$? + if [ "$inspect_rc" -ne 0 ] || [ "$hash_rc" -ne 0 ] || [ -z "$base" ] || [ -z "$health_hash" ]; then return 1; fi + printf '%s|healthcheck_sha256=%s\n' "${base#/}" "$health_hash" + done | LC_ALL=C sort > "$output" + pipeline_status="${PIPESTATUS[*]}" + case "$pipeline_status" in '0 0 0') return 0 ;; *) return 1 ;; esac +} + +verify_container_disposition() { + output=$1 + (cd "$script_dir/container-disposition" && sha256sum -c SHA256SUMS) \ + > "$record/container-disposition-checksum-latest.txt" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'container disposition checksum failed' 43; return $?; } + capture_container_fingerprint "$output" + rc=$? + [ "$rc" -eq 0 ] || { fail 'cannot capture current container fingerprint' 43; return $?; } + diff -u "$validated_preflight/container-fingerprint.psv" "$output" \ + > "$record/container-disposition-latest.diff" 2>&1 + rc=$? + [ "$rc" -eq 0 ] || { fail 'container identity, state, health, startup, network, restart policy, or health-check identity changed' 43; return $?; } + return 0 +} + capture_failed_unit_fingerprint() { output=$1 : > "$output" @@ -222,17 +256,7 @@ rerun_volatile_checks() { df -hT / /boot /var/cache/pacman/pkg /tmp > "$record/volatile-disk-space.txt" 2>&1 rc=$? [ "$rc" -eq 0 ] || { fail 'cannot verify current disk space' "$rc"; return $?; } - while IFS= read -r container; do - [ -n "$container" ] || continue - running=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) - health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null) - printf '%s|running=%s|health=%s\n' "$container" "${running:-missing}" "${health:-unknown}" \ - >> "$record/volatile-container-health.txt" - if [ "$running" != true ] || [ "$health" = unhealthy ]; then - fail "previously running container is unavailable: $container" 43 - return $? - fi - done < "$validated_preflight/running-containers-before.txt" + verify_container_disposition "$record/volatile-container-fingerprint.psv" || return $? : > "$record/volatile-public-health.txt" while IFS= read -r url; do metrics=$(curl --fail --silent --show-error --max-redirs 0 \ @@ -319,6 +343,8 @@ main() { rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot capture installed package inventory' "$rc"; return $?; } cp "$validated_preflight/bootloader-inventory.txt" "$validated_preflight/kernel-initramfs-before.txt" \ "$validated_preflight/running-containers-before.txt" "$validated_preflight/containers-all.psv" \ + "$validated_preflight/container-fingerprint.psv" "$validated_preflight/sonarr-unhealthy.allowlist.psv" \ + "$validated_preflight/directus-stopped.allowlist.psv" "$validated_preflight/restart-policy-no.psv" \ "$validated_preflight/critical-units-baseline.psv" "$validated_preflight/database-baseline.psv" "$record/" rc=$?; [ "$rc" -eq 0 ] || { fail 'cannot copy essential rollback baseline evidence' "$rc"; return $?; } verify_failed_unit_disposition "$record/failed-units-before-fingerprint.psv" || return $? diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/grub-btrfs-readiness.md b/docs/le-app-database-migration/phase2a-host-maintenance/grub-btrfs-readiness.md new file mode 100644 index 0000000..ab38b8b --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/grub-btrfs-readiness.md @@ -0,0 +1,78 @@ +# GRUB and grub-btrfs readiness + +Verified baseline: + +- GRUB has normal, fallback, and recovery entries for both `linux-zen` and + standard `linux`. +- Snapper snapshots 1, 2, and 3 exist. +- `/boot/grub/grub-btrfs.cfg` is absent, so there are currently no generated + snapshot entries. +- `grub-btrfs-snapper.path` is active and watching `/.snapshots`. + +The installed unit definitions are: + +```ini +# /usr/lib/systemd/system/grub-btrfs-snapper.path +[Unit] +Description=Monitors for new snapshots + +[Path] +PathModified=/.snapshots + +[Install] +WantedBy=multi-user.target +``` + +```ini +# /usr/lib/systemd/system/grub-btrfs-snapper.service +[Unit] +Description=Regenerate grub-btrfs.cfg + +[Service] +Type=oneshot +Environment="PATH=/sbin:/bin:/usr/sbin:/usr/bin" +EnvironmentFile=/etc/default/grub-btrfs/config +ExecStart=bash -c 'if [[ -z $(/usr/bin/findmnt -n / | /usr/bin/grep "\.snapshots") ]]; then if [ -s "${GRUB_BTRFS_GRUB_DIRNAME:-/boot/grub}/grub-btrfs.cfg" ]; then /etc/grub.d/41_snapshots-btrfs; else ${GRUB_BTRFS_MKCONFIG:-grub-mkconfig} -o ${GRUB_BTRFS_GRUB_DIRNAME:-/boot/grub}/grub.cfg; fi; fi' +``` + +The bounded July 12 journal contains only these two parser warnings and no +recorded service activation that generated snapshot entries: + +```text +Jul 12 17:16:42 TITANSERVER systemd[1]: /usr/lib/systemd/system/grub-btrfs-snapper.service:11: Ignoring unknown escape sequences in the ExecStart expression containing "\.snapshots". +Jul 12 17:18:23 TITANSERVER systemd[1]: /usr/lib/systemd/system/grub-btrfs-snapper.service:11: Ignoring unknown escape sequences in the ExecStart expression containing "\.snapshots". +``` + +The most supportable +inference is that the watcher was enabled after snapshots 1–3 or received no +qualifying modification afterward; absence of `grub-btrfs.cfg` confirms that the +generator has not completed successfully for those snapshots. + +The separately approved standalone generation and verification procedure is: + +```bash +/etc/grub.d/41_snapshots-btrfs +generate_rc=$? +printf 'grub_btrfs_generate_rc=%s\n' "$generate_rc" + +test -s /boot/grub/grub-btrfs.cfg +present_rc=$? +printf 'grub_btrfs_cfg_present_rc=%s\n' "$present_rc" + +grub-script-check /boot/grub/grub-btrfs.cfg +syntax_rc=$? +printf 'grub_btrfs_cfg_syntax_rc=%s\n' "$syntax_rc" + +grep -E 'snapshot|Snapshot|snapper|\.snapshots' /boot/grub/grub-btrfs.cfg +entries_rc=$? +printf 'grub_btrfs_entries_rc=%s\n' "$entries_rc" + +grep -q 'snapshots-btrfs' /boot/grub/grub.cfg +main_menu_link_rc=$? +printf 'grub_main_menu_snapshot_link_rc=%s\n' "$main_menu_link_rc" +``` + +This procedure is not approved or executed by the maintenance scripts. If the +last check fails, stop: making the submenu reachable would require a separately +approved `grub-mkconfig -o /boot/grub/grub.cfg`. Do not regenerate full GRUB as +an incidental readiness repair. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/maintenance-worksheet.md b/docs/le-app-database-migration/phase2a-host-maintenance/maintenance-worksheet.md new file mode 100644 index 0000000..4e4ddda --- /dev/null +++ b/docs/le-app-database-migration/phase2a-host-maintenance/maintenance-worksheet.md @@ -0,0 +1,32 @@ +# Phase 2A maintenance worksheet + +```text +PUBLIC_HEALTH_URLS='https://db.lasereverything.net/' +EXTRA_CRITICAL_UNITS='' +LE_PHASE2A_RECOVERY_READY= +LE_PHASE2A_BACKUPS_READY= + +maintenance_window= +user_notification= +rollback_decision_owner= +independent_console_access= +independent_root_access= +previous_kernel_boot_procedure= +``` + +Verified evidence: + +- protected Directus archive manifest: `PASS`, return code 0; +- Docker: 88 total, 87 running, retired `directus` intentionally stopped; +- sole unhealthy running container: reviewed unrelated `sonarr`; +- `makearmy-app` running; `bgbye` running and healthy; +- `le-directus-source` and `le-payload-pg` running; +- running restart-policy `no`: `castopod`, `castopod-redis`; +- DrKonqi disposition: exact checksummed 234-unit allowlist preserved; +- NoMachine removed; `nxserver.service` absent and not critical; +- GRUB: normal, fallback, and recovery entries for `linux-zen` and `linux`; +- Snapper snapshots 1–3 exist; `grub-btrfs.cfg` snapshot entries absent; +- `grub-btrfs-snapper.path` active and watching. + +The recovery and backup attestations must remain unset until the operator has +personally confirmed recovery access and completed the proportional backup plan. diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh index 10d9f1d..04d1e04 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/post-reboot-validate.sh @@ -71,18 +71,72 @@ validate_health_url_file() { return 0 } +capture_container_fingerprint() { + output=$1 + : > "$output" + docker ps -aq | while IFS= read -r cid; do + [ -n "$cid" ] || continue + base=$(docker inspect --format \ + '{{.Name}}|{{.Id}}|{{.Config.Image}}|{{.Image}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.State.StartedAt}}|{{.HostConfig.NetworkMode}}|{{.HostConfig.RestartPolicy.Name}}|{{index .Config.Labels "com.docker.compose.project"}}' \ + "$cid" 2>/dev/null) + inspect_rc=$? + health_hash=$(docker inspect --format '{{json .Config.Healthcheck}}' "$cid" 2>/dev/null | sha256sum | awk '{print $1}') + hash_rc=$? + if [ "$inspect_rc" -ne 0 ] || [ "$hash_rc" -ne 0 ] || [ -z "$base" ] || [ -z "$health_hash" ]; then return 1; fi + printf '%s|healthcheck_sha256=%s\n' "${base#/}" "$health_hash" + done | LC_ALL=C sort > "$output" + pipeline_status="${PIPESTATUS[*]}" + case "$pipeline_status" in '0 0 0') return 0 ;; *) return 1 ;; esac +} + compare_containers() { + capture_container_fingerprint "$post_record/container-fingerprint-after.psv" + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "container-fingerprint-after|rc=$rc"; return 0; fi : > "$post_record/container-comparison.psv" - while IFS= read -r container; do - [ -n "$container" ] || continue - running=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) - health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null) - printf '%s|running=%s|health=%s\n' "$container" "${running:-missing}" "${health:-unknown}" \ - >> "$post_record/container-comparison.psv" - if [ "$running" != true ] || [ "$health" = unhealthy ]; then - add_failure "container-regression|name=$container|running=${running:-missing}|health=${health:-unknown}" + : > "$post_record/restart-policy-no-attention.txt" + awk -F'|' '{print $1}' "$baseline/container-fingerprint.psv" > "$post_record/container-names-before.txt" + awk -F'|' '{print $1}' "$post_record/container-fingerprint-after.psv" > "$post_record/container-names-after.txt" + diff -u "$post_record/container-names-before.txt" "$post_record/container-names-after.txt" \ + > "$post_record/container-name-set.diff" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure 'container-name-set-changed'; fi + while IFS='|' read -r name old_id old_image_ref old_image_id old_state old_health old_started old_network old_restart old_project old_health_hash; do + [ -n "$name" ] || continue + current=$(awk -F'|' -v name="$name" '$1==name {print}' "$post_record/container-fingerprint-after.psv") + if [ -z "$current" ]; then + add_failure "container-missing|name=$name" + [ "$old_restart" = no ] && printf '%s|missing|manual-operator-attention-required\n' "$name" >> "$post_record/restart-policy-no-attention.txt" + continue fi - done < "$baseline/running-containers-before.txt" + IFS='|' read -r new_name new_id new_image_ref new_image_id new_state new_health new_started new_network new_restart new_project new_health_hash <> "$post_record/container-comparison.psv" + if [ "$old_id" != "$new_id" ] || [ "$old_image_ref" != "$new_image_ref" ] || \ + [ "$old_image_id" != "$new_image_id" ] || [ "$old_network" != "$new_network" ] || \ + [ "$old_restart" != "$new_restart" ] || [ "$old_project" != "$new_project" ] || \ + [ "$old_health_hash" != "$new_health_hash" ]; then + add_failure "container-identity-or-policy-changed|name=$name" + continue + fi + if [ "$name" = directus ]; then + [ "$new_state" = exited ] || add_failure "retired-directus-state|expected=exited|actual=$new_state" + elif [ "$name" = sonarr ]; then + if [ "$new_state" != running ]; then add_failure "sonarr-not-running|state=$new_state"; fi + case "$new_health" in unhealthy|healthy) ;; *) add_failure "sonarr-health-worsened|health=$new_health" ;; esac + elif [ "$old_state" = running ]; then + if [ "$new_state" != running ]; then + add_failure "previously-running-container-not-running|name=$name|state=$new_state" + [ "$old_restart" = no ] && printf '%s|%s|manual-operator-attention-required\n' "$name" "$new_state" >> "$post_record/restart-policy-no-attention.txt" + elif [ "$old_health" = healthy ] && [ "$new_health" != healthy ]; then + add_failure "healthy-container-regressed|name=$name|health=$new_health" + elif [ "$old_health" = no-healthcheck ] && [ "$new_health" != no-healthcheck ]; then + add_failure "container-healthcheck-identity-changed|name=$name|health=$new_health" + fi + fi + done < "$baseline/container-fingerprint.psv" return 0 } @@ -170,7 +224,7 @@ main() { return 12 fi baseline=$maintenance/preflight-record - required='running-containers-before.txt critical-units-baseline.psv database-baseline.psv health-urls.sanitized inventory-hashes.sha256 result.manifest result.manifest.sha256' + required='running-containers-before.txt container-fingerprint.psv directus-stopped.allowlist.psv sonarr-unhealthy.allowlist.psv restart-policy-no.psv critical-units-baseline.psv database-baseline.psv health-urls.sanitized inventory-hashes.sha256 result.manifest result.manifest.sha256' for file in $required; do if [ ! -f "$baseline/$file" ] || [ -L "$baseline/$file" ]; then printf 'HARD_STOP: missing baseline file %s\n' "$file" >&2 diff --git a/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh index 25b5342..02be0bf 100644 --- a/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh +++ b/docs/le-app-database-migration/phase2a-host-maintenance/preflight-read-only.sh @@ -65,7 +65,8 @@ capture_failed_unit_fingerprint() { "$id" "$type" "$load" "$active" "$sub" "$result" "$invocation" \ "$changed" "$fragment" "$code" "$status" done > "$output" - return ${PIPESTATUS[0]} + pipeline_status="${PIPESTATUS[*]}" + case "$pipeline_status" in '0 0 0') return 0 ;; *) return 1 ;; esac } validate_failed_unit_disposition() { @@ -78,8 +79,9 @@ validate_failed_unit_disposition() { capture_failed_unit_fingerprint "$record/failed-units-fingerprint.psv" rc=$? if [ "$rc" -ne 0 ]; then add_failure "failed-unit-fingerprint|rc=$rc"; return 0; fi - cp "$allowlist" "$allowlist_hashes" "$record/" + cp "$allowlist" "$record/drkonqi-failed-units.allowlist.psv" rc=$? + if [ "$rc" -eq 0 ]; then cp "$allowlist_hashes" "$record/failed-unit-disposition.SHA256SUMS"; rc=$?; fi if [ "$rc" -ne 0 ]; then add_failure "failed-unit-allowlist-copy|rc=$rc"; return 0; fi diff -u "$allowlist" "$record/failed-units-fingerprint.psv" > "$record/failed-unit-disposition.diff" 2>&1 rc=$? @@ -113,6 +115,62 @@ validate_snapper_readiness() { return 0 } +capture_container_fingerprint() { + output=$1 + : > "$output" + docker ps -aq | while IFS= read -r cid; do + [ -n "$cid" ] || continue + base=$(docker inspect --format \ + '{{.Name}}|{{.Id}}|{{.Config.Image}}|{{.Image}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.State.StartedAt}}|{{.HostConfig.NetworkMode}}|{{.HostConfig.RestartPolicy.Name}}|{{index .Config.Labels "com.docker.compose.project"}}' \ + "$cid" 2>/dev/null) + inspect_rc=$? + health_hash=$(docker inspect --format '{{json .Config.Healthcheck}}' "$cid" 2>/dev/null | sha256sum | awk '{print $1}') + hash_rc=$? + if [ "$inspect_rc" -ne 0 ] || [ "$hash_rc" -ne 0 ] || [ -z "$base" ] || [ -z "$health_hash" ]; then + return 1 + fi + printf '%s|healthcheck_sha256=%s\n' "${base#/}" "$health_hash" + done | LC_ALL=C sort > "$output" + pipeline_status="${PIPESTATUS[*]}" + case "$pipeline_status" in '0 0 0') return 0 ;; *) return 1 ;; esac +} + +validate_container_disposition() { + disposition_dir=$script_dir/container-disposition + (cd "$disposition_dir" && sha256sum -c SHA256SUMS) \ + > "$record/container-disposition-checksum.txt" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "container-disposition-checksum|rc=$rc"; return 0; fi + capture_container_fingerprint "$record/container-fingerprint.psv" + rc=$? + if [ "$rc" -ne 0 ]; then add_failure "container-fingerprint|rc=$rc"; return 0; fi + diff -u "$disposition_dir/all-containers.psv" "$record/container-fingerprint.psv" \ + > "$record/container-disposition.diff" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + add_failure 'container-disposition|identity-state-health-startup-network-restart-or-healthcheck-changed' + fi + total=$(wc -l < "$record/container-fingerprint.psv") + running=$(awk -F'|' '$5=="running"{n++} END{print n+0}' "$record/container-fingerprint.psv") + stopped=$(awk -F'|' '$5!="running"{n++} END{print n+0}' "$record/container-fingerprint.psv") + unhealthy=$(awk -F'|' '$6=="unhealthy"{n++} END{print n+0}' "$record/container-fingerprint.psv") + [ "$total" = 88 ] || add_failure "container-count|expected=88|actual=$total" + [ "$running" = 87 ] || add_failure "running-container-count|expected=87|actual=$running" + [ "$stopped" = 1 ] || add_failure "stopped-container-count|expected=1|actual=$stopped" + [ "$unhealthy" = 1 ] || add_failure "unhealthy-container-count|expected=1|actual=$unhealthy" + awk -F'|' '$6=="unhealthy" && $1!="sonarr" {print}' "$record/container-fingerprint.psv" \ + > "$record/unexpected-unhealthy-containers.psv" + [ ! -s "$record/unexpected-unhealthy-containers.psv" ] || add_failure 'container-health|additional-unhealthy-container' + awk -F'|' '$5!="running" && $1!="directus" {print}' "$record/container-fingerprint.psv" \ + > "$record/unexpected-stopped-containers.psv" + [ ! -s "$record/unexpected-stopped-containers.psv" ] || add_failure 'container-state|additional-stopped-container' + cp "$disposition_dir"/*.psv "$record/" + rc=$? + if [ "$rc" -eq 0 ]; then cp "$disposition_dir/SHA256SUMS" "$record/container-disposition.SHA256SUMS"; rc=$?; fi + [ "$rc" -eq 0 ] || add_failure "container-disposition-copy|rc=$rc" + return 0 +} + validate_health_urls() { : > "$record/health-urls.sanitized" if [ -z "${PUBLIC_HEALTH_URLS:-}" ]; then @@ -328,6 +386,7 @@ main() { "docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' | sort" mandatory_shell 'running-container-inventory' running-containers-before.txt \ "docker ps --format '{{.Names}}' | sort" + validate_container_disposition capture_unit_baseline capture_database_baseline validate_database_baseline @@ -350,8 +409,10 @@ main() { 'git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration status --short --branch; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse HEAD; git -C /srv/codex-work/lasereverything.net.db/.worktrees/payload-migration rev-parse origin/migration/le-app-database' mandatory_shell 'approved-transaction-integrity' approved-transaction-checksum.txt \ "cd '$script_dir/approved-transaction' && sha256sum -c SHA256SUMS" + mandatory_shell 'protected-directus-archive-integrity' directus-archive-verification.txt \ + "cd /srv/directus-archive/20260710-175408 && sha256sum --status -c SHA256SUMS && printf 'archive=/srv/directus-archive/20260710-175408|manifest=SHA256SUMS|result=PASS|return_code=0\\n'" - inventory_files='packages-before.txt failed-units-before.txt failed-units-fingerprint.psv failed-unit-disposition.diff failed-unit-allowlist-checksum.txt snapshots-subvolume.txt snapshots-stat.txt snapper-snapshots-before.txt btrfs-quota-observation.txt kernel-initramfs-before.txt bootloader-inventory.txt containers-all.psv running-containers-before.txt critical-units-baseline.psv database-baseline.psv database-readiness-before.txt health-urls.sanitized public-health.txt network-firewall-before.txt pacman-Dk-before.txt pacman-Qkk-before.txt pacman-Qkk-missing-or-unreadable-before.txt pacnew-pacsave-before.txt migration-git-state.txt approved-transaction-checksum.txt' + inventory_files='packages-before.txt failed-units-before.txt failed-units-fingerprint.psv failed-unit-disposition.diff failed-unit-allowlist-checksum.txt drkonqi-failed-units.allowlist.psv failed-unit-disposition.SHA256SUMS snapshots-subvolume.txt snapshots-stat.txt snapper-snapshots-before.txt btrfs-quota-observation.txt kernel-initramfs-before.txt bootloader-inventory.txt containers-all.psv running-containers-before.txt container-fingerprint.psv container-disposition.diff container-disposition-checksum.txt container-disposition.SHA256SUMS unexpected-unhealthy-containers.psv unexpected-stopped-containers.psv all-containers.psv running-containers.psv sonarr-unhealthy.allowlist.psv directus-stopped.allowlist.psv restart-policy-no.psv critical-units-baseline.psv database-baseline.psv database-readiness-before.txt health-urls.sanitized public-health.txt network-firewall-before.txt pacman-Dk-before.txt pacman-Qkk-before.txt pacman-Qkk-missing-or-unreadable-before.txt pacnew-pacsave-before.txt migration-git-state.txt approved-transaction-checksum.txt directus-archive-verification.txt' ( cd "$record" || return 1 sha256sum $inventory_files > inventory-hashes.sha256 From 82e64cec43f5f7087e4c14463693b5712faa82d1 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 21:01:17 -0400 Subject: [PATCH 10/14] add Phase 2B contained runtime artifacts --- .../environment-parity.md | 8 +- .../host-runtime-plan.md | 47 +- .../implementation-plan.md | 9 +- .../phase2b-contained-runtime/.gitattributes | 4 + .../INSTALL-ORDER.md | 15 + .../phase2b-contained-runtime/MANIFEST.sha256 | 31 + .../phase2b-contained-runtime/README.md | 45 + .../phase2b-contained-runtime/ROLLBACK.md | 12 + .../STATIC-SAFETY-AUDIT.md | 17 + .../inventory/SHA256SUMS | 9 + .../inventory/docker-networks.json | 2590 +++++++++++++++++ .../inventory/docker-networks.psv | 41 + .../inventory/host-network.txt | 885 ++++++ .../inventory/nftables-ruleset.txt | 702 +++++ .../inventory/runtime-state.txt | 21 + .../inventory/runtime-versions.txt | 41 + .../inventory/workspace-acls.txt | 32 + .../inventory/workspace-mounts.txt | 0 .../inventory/workspace-tree.psv | 8 + .../etc/le-app-codex-runtime/resolv.conf | 4 + .../etc/nftables.d/50-le-app-codex.nft | 54 + .../systemd/system/le-app-codex-netns.service | 11 + .../50-le-app-codex-resources.conf | 7 + .../50-le-app-codex-containment.conf | 40 + .../home/.config/docker/daemon.json | 16 + .../home/.config/systemd/user/docker.service | 34 + .../srv/le-app-codex/home/.docker/config.json | 4 + .../meta.json | 10 + .../local/libexec/dockerd-rootless-29.6.1.sh | 255 ++ .../usr/local/libexec/le-app-codex-netns | 40 + .../tests/00-read-only-preflight.sh | 28 + .../tests/00-root-preinstall-validate.sh | 101 + .../tests/10-inert-containment.sh | 41 + .../tests/20-rootless-docker.sh | 35 + .../tests/run-inert-without-user-manager.sh | 20 + 35 files changed, 5177 insertions(+), 40 deletions(-) create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/.gitattributes create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/README.md create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/SHA256SUMS create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.json create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.psv create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/host-network.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/nftables-ruleset.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-state.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-versions.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-acls.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-mounts.txt create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-tree.psv create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/le-app-codex-runtime/resolv.conf create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/nftables.d/50-le-app-codex.nft create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/le-app-codex-netns.service create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/docker/daemon.json create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/systemd/user/docker.service create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/config.json create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/dockerd-rootless-29.6.1.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/le-app-codex-netns create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/00-read-only-preflight.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/10-inert-containment.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/20-rootless-docker.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/run-inert-without-user-manager.sh diff --git a/docs/le-app-database-migration/environment-parity.md b/docs/le-app-database-migration/environment-parity.md index 485f016..606f1c0 100644 --- a/docs/le-app-database-migration/environment-parity.md +++ b/docs/le-app-database-migration/environment-parity.md @@ -152,22 +152,22 @@ Current host observation and planning checkpoint: - the default Docker context does not provide a usable daemon/security result to this user; - no enabled user-level rootless Docker service was detected; - no persistent `le-app-codex` namespace or project nftables policy is deployed; -- approved DNS remains unresolved; the host resolver currently includes `192.168.10.1`, which is an observation rather than approval; +- root inventory is complete; Quad9 IPv4 `9.9.9.9` and `149.112.112.112` are approved, while observed LAN resolver `192.168.10.1` remains denied with no LAN exception; - numerous existing Docker IPv4 and IPv6 bridge ranges must be dynamically inventoried and denied rather than represented only by a stale list. Therefore a `sol6_vi`-owned daemon is rejected: it could bind-mount those readable production secrets. Milestone 0 requires the isolated account and host changes described in `host-runtime-plan.md`, and work must stop before performing them. An administrator must approve/provision: 1. dedicated `le_app_codex` identity and restricted filesystem ACLs; -2. an explicitly selected package path: scheduled full host maintenance or a clean-chroot/current-system-compatible investigation, never a partial Arch upgrade; +2. the completed and verified Phase 2A package-maintenance baseline, without rerunning that transaction; 3. a checksum-pinned Moby rootless launcher matched to the installed Docker release, not the unmodified AUR extras package or its global sysctl hook; 4. instance-specific containment on `user@1200.service`, the proposed `user-1200.slice` resource limits, and no lingering; 5. a root-created IPv4-only namespace with default-deny egress and dynamic denial of host, LAN, WireGuard, metadata, private, loopback, Docker bridge, SMTP, and IPv6 access; 6. inert acceptance tests before starting the user manager, Docker, or Codex; 7. deterministic CPU BGBye first; optional GPU access only after core acceptance; 8. user/browser-profile-scoped development CA trust; -9. a separately approved disk-growth limit for images, caches, snapshots, media, PostgreSQL, and test artifacts. +9. deferred hard disk-growth enforcement for images, caches, snapshots, media, PostgreSQL, and test artifacts; this is not a Phase 2B installation blocker and existing visibility/resource controls are not a hard quota. -Lingering must remain disabled. No production socket forwarding, Docker-group membership, broad sudo rule, daemon TCP exposure, system-service `dockerd`, or bind mount into production paths is acceptable. See `host-runtime-plan.md` for the approval-gated lifecycle and unresolved DNS, address-inventory, authentication, package, and disk-quota decisions. +Lingering must remain disabled. No production socket forwarding, Docker-group membership, broad sudo rule, daemon TCP exposure, system-service `dockerd`, or bind mount into production paths is acceptable. See `host-runtime-plan.md` for the approval-gated lifecycle, deferred nonblocking storage quota, and unresolved authentication decision. ## One-command workflows diff --git a/docs/le-app-database-migration/host-runtime-plan.md b/docs/le-app-database-migration/host-runtime-plan.md index a8faeac..e7a8aea 100644 --- a/docs/le-app-database-migration/host-runtime-plan.md +++ b/docs/le-app-database-migration/host-runtime-plan.md @@ -1,6 +1,6 @@ # Milestone 0 host runtime and security plan -Status: corrected planning checkpoint, 2026-07-12. Nothing in this document authorizes execution. Phase 1 created only the inert `le_app_codex` identity and directory skeleton. Package installation, systemd changes, namespace creation, firewall changes, user-manager startup, Docker startup, Codex authentication, repository/source copying, and production changes remain separately approval-gated. +Status: Phase 2A completed and verified, 2026-07-12; Phase 2B is review-only. Nothing in this document authorizes Phase 2B execution. Phase 1 created only the inert `le_app_codex` identity and directory skeleton. Phase 2B systemd changes, namespace creation, firewall changes, user-manager startup, Docker startup, Codex authentication, repository/source copying, and production changes remain separately approval-gated. This document supersedes the earlier runtime proposal. In particular, the project will not use a system service running `dockerd` with `User=le_app_codex`, will not enable lingering, and will not install the unmodified AUR `docker-rootless-extras` package. @@ -42,26 +42,15 @@ Root starts and stops `user@1200.service` explicitly after inert acceptance test A system unit with `User=le_app_codex` must not run `dockerd`. That model bypasses the supported per-user lifecycle and does not provide the required containment of Codex and every sibling user process. -## Package decision gate and pinned launcher +## Completed Phase 2A and pinned launcher -The last isolated preview of: +Phase 2A host maintenance is complete and supersedes the provisional package-maintenance gate. The host booted successfully on `7.1.3-zen1-2-zen`; both standard and Zen 7.1.3 kernels and their NVIDIA DKMS modules are installed. `rootlesskit 3.0.1-1`, `slirp4netns 1.3.4-1`, `libslirp 4.9.3-1`, and `openai-codex 0.144.1-2` are installed; `/usr/bin/codex` reports `codex-cli 0.144.1`. -```text -pacman -Syu --needed rootlesskit slirp4netns openai-codex -``` - -was refreshed after removal of unused NoMachine and is now a 220-package full host upgrade (215 upgrades and 5 new packages) affecting both installed kernels and OpenSSH. It requires reboot planning, service-health checks, `.pacnew` review, explicit Snapper snapshot verification, and separate operator approval. The checksummed baseline is in `phase2a-host-maintenance/approved-transaction/`. A partial Arch upgrade is not acceptable. - -The package paths are: - -1. schedule and approve the complete refreshed host upgrade; or -2. investigate clean-chroot, checksum-pinned packages compatible with the current system, without partially upgrading repository packages. - -The operator provisionally selected Path A on 2026-07-12 as the preferred Phase 2 package path because it is the shortest safe route consistent with Arch's complete-upgrade model. This records planning direction only: the transaction, maintenance window, package execution, and reboot remain separately approval-gated. The uncommitted review package is under `docs/le-app-database-migration/phase2a-host-maintenance/`. +Post-maintenance verification also established: Laser Everything HTTP 200; NoMachine removed; stale 6.19 DKMS state removed; Snapper repaired with valid Pacman pre/post snapshots; protected Directus archive checksum valid; retired Directus stopped with restart policy `no`; `/root/server-compose-runner.sh` no longer references `lasereverything.net.forms`; and no unexpected failed system units. The retained review/evidence package is under `phase2a-host-maintenance/`. Do not install the unmodified AUR `docker-rootless-extras` package. Its install hook reloads global sysctls, which exceeds this project's boundary. Do not curl an installer into a shell. -Install only a reviewed Moby `contrib/dockerd-rootless.sh` matching the Docker engine actually installed after the chosen package path. At the current checkpoint Docker is `29.6.1`; the candidate is: +Install only a reviewed Moby `contrib/dockerd-rootless.sh` matching the Docker engine installed by the completed Phase 2A maintenance. Docker is `29.6.1`; the reviewed artifact is: ```text Moby tag: docker-v29.6.1 @@ -71,7 +60,7 @@ SHA-256: 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46 Owner/mode: root:root 0755 ``` -Revalidate the installed Docker version, upstream tag, source content, checksum, and launcher compatibility immediately before installation. If Docker changes, this filename and checksum are invalid until a matching launcher is reviewed. A clean-chroot package containing only the pinned launcher is acceptable; it must have no global sysctl hook, setup tool, service enablement, or post-install execution. +Docker was revalidated as Arch package `1:29.6.1-1`, client `29.6.1` build `8900f1d330`, daemon binary `29.6.1` build `8ec5ab355a`. The upstream tag, source, and checksum were re-fetched and revalidated on 2026-07-12. Revalidate all four again immediately before installation. If Docker changes, this filename and checksum are invalid until a matching launcher is reviewed. A clean-chroot package containing only the pinned launcher is acceptable; it must have no global sysctl hook, setup tool, service enablement, or post-install execution. ## User-manager filesystem and device containment @@ -132,7 +121,7 @@ ProtectControlGroups=yes ProcSubset=pid ``` -`ProtectProc=invisible` and hostname isolation remain acceptance-test candidates. They may be added only after the user manager, RootlessKit, Docker, Codex, and test containers pass with them. The final contract must prevent unrelated host-process inspection without breaking the supported runtime. +`ProtectProc=invisible` and hostname isolation remain acceptance-test candidates. Compatibility must not be assumed; they may remain only after the user manager, RootlessKit, Docker, Codex, and test containers pass with them. The final contract must prevent unrelated host-process inspection without breaking the supported runtime. ## Resource boundary @@ -150,7 +139,7 @@ TasksMax=4096 These limits cover Codex, RootlessKit, `dockerd`, containerd, builds, jobs, and all containers. Validate generated properties and cgroup delegation before relying on the limits. -Disk-growth containment remains unresolved. Btrfs quotas remain intentionally disabled. The repaired `/.snapshots` Btrfs subvolume and successful Snapper snapshots 1, 2, and 3 establish snapshot operation, not quota accounting. Do not enable quotas, run `snapper setup-quota`, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit incidentally. Filesystem visibility constrains where the account may write but does not cap growth within `/srv/le-app-codex`. The operator must separately choose and approve a disk-quota policy. +Hard disk-growth enforcement is deferred and is not a Phase 2B installation blocker. Btrfs quotas remain intentionally disabled. The repaired `/.snapshots` Btrfs subvolume and successful Snapper snapshots 1, 2, and 3 establish snapshot operation, not quota accounting. Do not enable quotas, run `snapper setup-quota`, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit incidentally. Mandatory filesystem visibility and resource controls constrain access and CPU/memory/tasks but do not cap growth within `/srv/le-app-codex` and are not a hard storage quota. Soft planning budgets remain useful for monitoring: 80 GiB container runtime, 25 GiB PostgreSQL, 15 GiB media, 10 GiB legacy uploads, 35 GiB BGBye models, 10 GiB temporary data, 5 GiB browser artifacts, 10 GiB migration reports, and 10 GiB checkout/home metadata. Startup should fail when host/project free-space thresholds are unsafe, but soft checks do not replace a hard quota. @@ -219,15 +208,16 @@ Docker client configuration lives only below `/srv/le-app-codex/home/.docker`. T Root creates and owns the persistent namespace `/run/netns/le-app-codex`. The complete `user@1200.service` enters it through `NetworkNamespacePath`; the user cannot replace the namespace or firewall policy. -Current read-only findings at this checkpoint: +Current read-only findings from the checksummed root inventory: -- approved DNS resolver IPv4 addresses remain unresolved; -- the host resolver currently includes `192.168.10.1`, but observation is not approval; +- the operator approved Quad9 IPv4 `9.9.9.9` and `149.112.112.112`; +- the host resolver includes `192.168.10.1`, but it remains denied and no LAN resolver exception is proposed; - no persistent `le-app-codex` network namespace currently exists; - no project-specific nftables rules are deployed; -- the host has numerous existing Docker IPv4 and IPv6 bridge ranges. +- observed host public/NAT IPv4 is `74.67.173.56`, with host/LAN/WireGuard addresses `192.168.10.151`, `192.168.10.0/24`, `10.98.0.2`, and `10.98.0.0/24`; +- all 41 Docker networks and their IPv4/IPv6 IPAM ranges are retained in the Phase 2B inventory and covered by the private/IPv6 denials. -All host addresses, routes, LAN/VPN/WireGuard/management networks, public/NAT addresses, Docker bridges, and resolver endpoints must be dynamically inventoried immediately before drafting the final rules. Future policy must deny every observed Docker IPv4 and IPv6 bridge range and must not rely only on a stale hard-coded list. +All host addresses, routes, LAN/VPN/WireGuard/management networks, public/NAT addresses, Docker bridges, resolver endpoints, nftables rules, ACLs, and mounts were inventoried by root on 2026-07-12 at 20:24 EDT. The complete checksummed record is retained with the Phase 2B artifacts. The preflight dynamically repeats this inventory immediately before installation and hard-stops on drift. The namespace is IPv4-only: assign no IPv6 address or route and reject IPv6 on the project veth. Do not change host-global IPv6 or sysctls. @@ -250,15 +240,13 @@ Explicitly reject: - TCP 25, 465, and 587; - all IPv6 traffic. -Known addresses requiring explicit coverage include `192.168.10.151`, `10.98.0.2`, and `2603:7080:7500:1279:75aa:d64d:4cf0:8f10`; these are not a complete inventory. - -Required operator inputs are approved DNS resolver IPv4 addresses, the complete host/public/NAT address set, every LAN/VPN/WireGuard/bridge/management subnet, and any approved host DNS/proxy endpoint. Do not finalize or deploy nftables without those inputs and a fresh read-only ruleset inventory. Forge SSH is outside the sandbox; no TCP/22 or LAN exception is proposed. +Explicit coverage includes `192.168.10.151`, `10.98.0.2`, `74.67.173.56`, and `2603:7080:7500:1279:75aa:d64d:4cf0:8f10`, plus the inventoried network ranges. Public/NAT address drift is a fail-closed configuration-review condition before later namespace restarts or policy regeneration. Forge SSH is outside the sandbox; no TCP/22 or LAN exception is proposed. ## Acceptance gates and order of operations Every stage stops for review. No real migration data, nonproduction credentials, or Codex authentication enters the boundary until inert tests pass. -1. Resolve and approve the package path; install only the approved packages in a separately controlled stage. +1. Verify the completed Phase 2A state and do not rerun its package transaction. 2. If a full host upgrade was selected, reboot and complete production health verification. 3. Install only the version-matched, checksum-pinned Moby launcher. 4. Install the resource slice and user-manager containment files without starting `user@1200.service`. @@ -313,9 +301,6 @@ Never recursively copy or `rsync` the nonproduction workspace into production. C ## Unresolved decisions checklist -- Phase 2 package path: Path A is provisionally preferred; package execution and reboot are not approved. -- Approved DNS resolver IPv4 addresses; `192.168.10.1` is currently observed but not approved. -- Complete current host, public/NAT, LAN, management, VPN, WireGuard, Docker bridge, and other container-network inventory. - Codex authentication: device authentication or dedicated API key. - Disk-growth enforcement: Btrfs quota policy or another separately approved mechanism. - Whether deferred process/hostname hardening options pass acceptance tests. diff --git a/docs/le-app-database-migration/implementation-plan.md b/docs/le-app-database-migration/implementation-plan.md index 194700e..0dd5985 100644 --- a/docs/le-app-database-migration/implementation-plan.md +++ b/docs/le-app-database-migration/implementation-plan.md @@ -1,6 +1,6 @@ # Laser Everything database transition implementation plan -Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runtime approval gate. +Status: Phase 2A complete; Phase 2B contained-runtime artifacts are review-only. Work stops at the Milestone 0 Phase 2B approval gate. ## Milestone 0 — complete reproducible application environment @@ -10,16 +10,15 @@ Status: revised pre-implementation plan. Work stops at the Milestone 0 host-runt 2. **Host runtime gate — requires human approval/provisioning** - Preserve the completed inert `le_app_codex` Phase 1 identity/workspace; do not rerun bootstrap artifacts. - - Preserve the provisional Path A selection: prepare a scheduled complete Arch host upgrade, but require separate approval for the final transaction and reboot; never perform a partial Arch upgrade or install the unmodified AUR extras package. + - Record Phase 2A host maintenance as complete and verified; its former provisional package-maintenance gate is superseded. Do not rerun it or install the unmodified AUR extras package. - Use a version-matched, checksum-pinned Moby launcher and a genuine per-user Docker service managed through root-controlled `user@1200.service`; never enable lingering or run `dockerd` from a system service with `User=le_app_codex`. - Apply containment and the proposed `user-1200.slice` limits to the complete user manager; accept only `unix:///run/user/1200/docker.sock`. - Create an IPv4-only root-owned namespace with default-deny egress and explicit host/LAN/WireGuard/metadata/private/loopback/bridge/SMTP/IPv6 denial after DNS and complete address inventories are approved. - Restrict production environment files currently readable to `sol6_vi`; verify the development account cannot traverse production paths. - Use deterministic CPU BGBye initially; defer optional rootless GPU provisioning. - Approve browser-profile-scoped local CA trust for HTTPS testing. - - Resolve authentication and disk-quota policy separately; confirm nonproduction storage/capacity locations. - - Use the refreshed post-NoMachine 220-package checksummed baseline; require exact disposition of the reviewed 234 DrKonqi desktop crash-processing failures and hard-stop on any fingerprint change or other failed unit. - - Verify the repaired `/.snapshots` Btrfs subvolume and create, number, and independently verify an explicit pre-upgrade Snapper snapshot; do not enable Btrfs quotas or treat unavailable quota accounting as snapshot failure. + - Resolve authentication separately. Defer hard disk-growth enforcement without treating mandatory filesystem visibility and CPU/memory/task limits as a storage quota or Phase 2B installation blocker; confirm nonproduction storage/capacity locations. + - Preserve Phase 2A evidence: successful Zen 7.1.3 boot, dual-kernel NVIDIA DKMS, NoMachine removal, stale DKMS cleanup, repaired Snapper pre/post snapshots, protected archive verification, retired Directus disposition, runner cleanup, HTTP 200, and no unexpected failed units. - Execute every inert positive and negative preflight in `host-runtime-plan.md` before starting the user manager, Docker, Codex, or application work. 3. **Reversible canonical-path consolidation — separately approval-gated** diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/.gitattributes b/docs/le-app-database-migration/phase2b-contained-runtime/.gitattributes new file mode 100644 index 0000000..5bf9e05 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/.gitattributes @@ -0,0 +1,4 @@ +# Preserve checksummed generated evidence and staged payload bytes verbatim. +inventory/** -whitespace +payload/** -whitespace +tests/** -whitespace diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md b/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md new file mode 100644 index 0000000..c82dc16 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md @@ -0,0 +1,15 @@ +# Exact staged installation and acceptance order + +Every numbered boundary requires review and explicit approval. Nothing here authorizes execution. + +1. From this directory, run only `tests/00-root-preinstall-validate.sh` as root and require zero failures. It atomically checks both manifests, staged nftables/systemd/shell/JSON, exact Phase 1 ownership/modes, absent approval marker/installed payload/manager/process/socket/linger, and public/NAT address drift. Save output outside production. +2. Preserve the approved Quad9 DNS pair `9.9.9.9` and `149.112.112.112`, observed public/NAT address denial `74.67.173.56`, and verified root inventory. Quad9 requires no LAN exception; `192.168.10.1` remains denied. Any different value or public/NAT drift requires artifact edits, a new manifest, and another review. +3. Copy only the manifest-listed launcher to `/usr/local/libexec`, mode `0755`, `root:root`; rehash the installed file. +4. Copy only the slice and `user@1200.service` drop-ins, resolver file, namespace helper/unit, and nftables fragment to their manifest paths with `root:root`, directories/files `0755/0644`, helper `0755`. After exact-value approval, create root-owned mode `0644` `/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED` containing the artifact-manifest checksum and approval timestamp. Do not start anything. Run `systemd-analyze verify` and `nft --check -f`; inspect the merged `systemctl cat`/properties. +5. Copy the Docker user unit/config/context and tests below `/srv/le-app-codex`; set directories `0750`, config/unit/tests `0640` except tests `0750`, all owned `1200:1200`. Do not create credentials, checkout, or source copies. +6. Run `systemctl daemon-reload`. Confirm the user manager remains inactive, lingering absent, and socket/process absent. +7. Start only `le-app-codex-netns.service`. Inspect namespace identity, IPv4-only addresses/routes, nftables counters/rules, host routing, and production Docker health. Stop on any drift. +8. Run `tests/run-inert-without-user-manager.sh`. This starts only a finite transient test process, never `user@1200.service`, Docker, or Codex. Require every filesystem/process/cgroup/device/socket/network assertion to pass. +9. Review the inert evidence. Only a later explicit runtime-acceptance approval may start `user@1200.service`, enable `docker.service` within its controlled lifetime, or run `tests/20-rootless-docker.sh` as UID 1200. +10. The later positive/negative Docker test uses only `unix:///run/user/1200/docker.sock`, disposable pinned images, and teardown. It must prove rootless/data-root/context behavior and denial of rootful sockets, private/host/LAN/VPN/metadata/SMTP/IPv6, and published ports. +11. Stop the manager after testing. Codex authentication, Codex startup, repository/source/credential copying, application startup, and production changes remain out of scope. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 b/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 new file mode 100644 index 0000000..e02d413 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 @@ -0,0 +1,31 @@ +f5bc35fd3767c0cbc72495b1dea283e8237789dd383ed754bf629ca09379ab97 .gitattributes +e4c381697c727f364c2a212fede2f703cfab8e0a57aaa4fa98550478a76eef55 INSTALL-ORDER.md +a62100ff7046a6e7d89adb8d2ab6bdf4ef7a72153df7cc439c8b00b42c7df347 README.md +b72075836eddd338eb1402beab775b4dc21a61d315e78cd8385f719578924293 ROLLBACK.md +b8a5f38f26fa76db6281a14de2baae3505cd78428b4c81a27a23ea89e256f321 STATIC-SAFETY-AUDIT.md +05bdf987ad0049118acf9371c6affae0d7bf65bfcb869d297986860ee20dfac4 inventory/SHA256SUMS +e6f6da94601927a23d252f32de93e92c99d31fb2d2cbd9b00f2b88224dbb684e inventory/docker-networks.json +9a5d343092c8cf473f0e4d06cb6848f1788aa8bc41b1b3fb7412faea7ea7b48c inventory/docker-networks.psv +67178aa964dfa23116984dfc5c677f8c7ef8631d5d7de989193a54a620ceaec1 inventory/host-network.txt +4695953ecd1d859198d1a5cbb6242d10ba27493ab3b1ffb3f8f77da39fd458e0 inventory/nftables-ruleset.txt +0a0db228df70f4d15e09f5c96bd0d5ac9952f4300b898266d8963225a9815988 inventory/runtime-state.txt +4d5c3252ac4f9f537ce3f69110127f13a88ea424cbc69a46ca75448341e2bae7 inventory/runtime-versions.txt +1565a8f5b5f08059ab9df69d78fed1b8190ccf674a9d368a436af63c03b804a8 inventory/workspace-acls.txt +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 inventory/workspace-mounts.txt +362b311c42fdd35bda6bcc9639403dd0f1dca17bc6469ccc9c86a9580021f046 inventory/workspace-tree.psv +1bc6ed97f33210f0d9337c7593082aaad46efdc26c86af3603f4c7c6ed88933c payload/etc/le-app-codex-runtime/resolv.conf +4289b705dbd786281daccb6d5aa660c27b83441f1a34d0cd18590666124be386 payload/etc/nftables.d/50-le-app-codex.nft +628370fd3cbb5c4572866f820fc404c23ce63505d5ac704e0b2019b3c1e72656 payload/etc/systemd/system/le-app-codex-netns.service +ea0c280203614c3e308013a8d51271bfa8adafd517f65cdf3224d2385244af6d payload/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf +30d9fdc95642ba6c9b496cc08fbfc5c9a3c8349d4c2b3331a1ff2bf4de06a383 payload/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf +44ec71cb10ef3885484a0e53a48c4cef1d028d417f4757dd7be6c95b556b92e8 payload/srv/le-app-codex/home/.config/docker/daemon.json +97aabdffb67c48379a0e5d524d8a728154cc197efa82c163608de2387b211b3b payload/srv/le-app-codex/home/.config/systemd/user/docker.service +9975f57e7cd150744f0acfe6d38c7074aa1d302465e50ca19015eedf04d6587d payload/srv/le-app-codex/home/.docker/config.json +612b81ebb800af1fe5261d1d29284136977ce278eb689d8cdee0eec2c2969cd6 payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json +904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46 payload/usr/local/libexec/dockerd-rootless-29.6.1.sh +9aed833454a78dc99af6c18da1cf09660ef717964adfd309f61f043b53c05464 payload/usr/local/libexec/le-app-codex-netns +4f94d88283207b2bfa849f4e5b126efa8872be97e59edd1f700b1b0a80623035 tests/00-read-only-preflight.sh +f7629ad1178720c6046bf73d2babdcf7f8db6398ded172c5a088340f38d077a2 tests/00-root-preinstall-validate.sh +9a573d539da46fbd683da60920596118174fdcf053f79f037ba21aee2deab82d tests/10-inert-containment.sh +0ec4d5d955e55fa8eb053fe2efa9a60509ada31afa48b71b29652dfacbf4d1c2 tests/20-rootless-docker.sh +1cf5dfa80b8d00de3ef1a47d84c30a891f4c6192345d5fc9247c4ab7d4f40f31 tests/run-inert-without-user-manager.sh diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/README.md b/docs/le-app-database-migration/phase2b-contained-runtime/README.md new file mode 100644 index 0000000..cc2cdf7 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/README.md @@ -0,0 +1,45 @@ +# Phase 2B contained Codex runtime — review only + +Status: complete staged review set, 2026-07-12. **Do not install or execute it.** No Phase 2B file has been copied to a host target; no namespace, nftables table, service, user manager, Docker daemon, container, Codex process, authentication, checkout, migration source, credential, or production change was created. + +## Read-only verification record + +- Docker was revalidated before launcher selection: package `1:29.6.1-1`; client `29.6.1` build `8900f1d330`; daemon binary `29.6.1` build `8ec5ab355a`. +- The exact Moby `docker-v29.6.1` `contrib/dockerd-rootless.sh` was fetched for review and reproduced SHA-256 `904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46`. +- Future acceptance images are pinned to OCI indexes: Alpine 3.23.3 `sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659` and nginx 1.29.4-alpine `sha256:4870c12cd2ca986de501a804b4f506ad3875a0b1874940ba0a2c7f763f1855b2`. +- The root inventory at 2026-07-12 20:24 EDT is retained verbatim under `inventory/`; every checksum in its original `SHA256SUMS` passes. Host interfaces include `192.168.10.151/24` on `enp4s0`, `10.98.0.2/24` on `wg0`, public IPv6 `2603:7080:7500:1279:75aa:d64d:4cf0:8f10/64`, loopback, and the Docker bridges below. Observed public IPv4 is `74.67.173.56`. The observed gateway/router resolver is `192.168.10.1`; it is **not approved DNS** by observation. +- `/etc/resolv.conf` also observes link-local IPv6 resolver `fe80::3e8c:f8ff:fef4:2590%enp4s0`. The proposed namespace is IPv4-only, so it is not usable there. +- Observed Docker bridge IPv4 subnets are `10.1.0.0/16`, `10.2.0.0/16`, `10.3.0.0/16`, `10.4.0.0/16`, `10.9.0.0/16`, `10.10.0.0/16`, `10.13.0.0/16`, `10.16.0.0/16` through `10.20.0.0/16`, `10.24.0.0/16`, `10.26.0.0/16` through `10.32.0.0/16`, `10.40.0.0/16`, `10.41.0.0/16`, `10.55.0.0/16`, `10.99.0.0/16`, and `172.17.0.0/16` through `172.31.0.0/16` except `172.24` is present and all ranges in that continuous interval were observed. The policy denies their covering RFC1918 ranges and the preflight re-inventories them. +- Observed Docker bridge IPv6 includes `fd00:1::/64`, `fd00:3::/64`, `fd00:4::/64`, `fd00:9::/64`, `fd00:10::/64`, `fd00:16::/64`, `fd00:17::/64`, `fd00:18::/64`, `fd00:20::/64`, `fd00:24::/64`, `fd00:26::/64` through `fd00:31::/64`, `fd00:40::/64`, `fd00:41::/64`, and link-local `fe80::/64` addresses. Namespace IPv6 is disabled and forward traffic is dropped. +- `user@1200.service` is loaded/static but inactive/dead; only the packaged `10-login-barrier.conf` drop-in exists. UID 1200 is not logged in or lingering; no UID 1200 process was observed. `/run/user/1200/docker.sock` is absent. +- `/srv/le-app-codex` is `root:le_app_codex` mode `0750`; `home`, checkout, runtime, testing, and build directories are UID/GID 1200 with mode `0700`; the source directory is root-owned `0550`; nonproduction secrets are root-owned `0750`. ACLs add no access beyond those modes. The root inventory found no project-rooted mounts. +- Root Docker inspection records 41 networks, all covered by the private/IPv6 denials. The complete nftables export has no pre-existing Phase 2B table, chain, namespace, or interface. `tests/00-read-only-preflight.sh` requires a fresh matching inventory immediately before installation. + +## Artifact map + +The `payload/` tree mirrors proposed absolute installation paths and makes each file separately reviewable: + +- pinned Moby launcher; +- aggregate `user-1200.slice` resource drop-in; +- complete `user@1200.service` filesystem/process/device containment drop-in; +- root-owned IPv4-only namespace unit/helper; +- private read-only resolver configuration using the proposed public Quad9 IPv4 pair; +- separate default-deny nftables tables with reviewed host/network values; +- genuine rootless Docker user service; +- daemon and client context configuration accepting only `unix:///run/user/1200/docker.sock`; +- preflight, inert containment, and rootless Docker positive/negative tests; +- exact installation/acceptance order and Phase 2B-only rollback. + +There are no remaining documentation sentinels. The operator approved Quad9's malware-blocking, DNSSEC-validating IPv4 pair, `9.9.9.9` and `149.112.112.112`. It requires **no LAN-address exception**; `192.168.10.1` and all of `192.168.0.0/16` remain denied. The root-created approval marker is intentionally absent because installation has not begun. Observed public/NAT IPv4 `74.67.173.56` remains explicitly denied; drift is a fail-closed configuration-review condition before later namespace restarts or policy regeneration. + +## Unresolved operator inputs + +1. Hard disk-growth enforcement is deferred and is not a Phase 2B installation blocker. Filesystem visibility and CPU/memory/task controls remain mandatory but are not a hard storage quota. +2. Whether `ProtectProc=invisible` passes inert and runtime acceptance without breaking rootless Docker; compatibility must not be assumed. +3. Later, separate approvals for starting the user manager/runtime and for the Codex authentication method. No Forge SSH exception, TCP Docker endpoint, rootful socket, LAN access, direct SMTP, or inbound publication is proposed. + +## Validation status + +The root pre-install validator passed on TITANSERVER with zero failures: both manifests, staged nftables syntax, staged systemd units/drop-ins, shell/JSON syntax, Phase 1 ownership/modes, mount absence, installed-path absence, inactive user manager, absent linger/process/socket, and unchanged public/NAT IPv4 all passed. Missing `/usr/local/libexec` warnings were expected for the uninstalled payload. + +See `STATIC-SAFETY-AUDIT.md`, `INSTALL-ORDER.md`, `ROLLBACK.md`, `tests/`, `inventory/`, and `MANIFEST.sha256`. Any edit invalidates the manifest and requires another review. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md b/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md new file mode 100644 index 0000000..023438f --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md @@ -0,0 +1,12 @@ +# Phase 2B-only rollback + +Do not remove Phase 1 identity/workspace, Phase 2A packages, kernels, snapshots, production resources, or unrelated nftables objects. + +1. Stop `user@1200.service`; confirm UID 1200 has no processes. This is the only user-manager stop in scope. +2. Stop `le-app-codex-netns.service`. Its `ExecStop` removes only `lecodex-host` and `/run/netns/le-app-codex` and deletes only the two `le_app_codex*` nftables tables. +3. Remove only the Phase 2B files enumerated in `MANIFEST.sha256` and `/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED`, preserving the pre-existing `/srv/le-app-codex` skeleton. +4. Run `systemctl daemon-reload` and `systemctl reset-failed le-app-codex-netns.service`. +5. Confirm there is no UID 1200 process, socket, namespace, veth, project nftables table, containment/resource drop-in, launcher, Docker user unit, daemon/client config, or Phase 2B test copy. +6. Confirm lingering is still disabled and production/rootful Docker remains healthy and unchanged. + +If a file existed before Phase 2B, stop: this rollback package intentionally has no overwrite/restore path because installation must have failed closed on pre-existence. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md b/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md new file mode 100644 index 0000000..53bcec9 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md @@ -0,0 +1,17 @@ +# Static safety audit + +Status: pass for final review; execution remains approval-gated. + +- **Endpoint:** daemon service, daemon configuration, client context, environment, and tests name only `unix:///run/user/1200/docker.sock`. No TCP, SSH, rootful, fallback, or inherited default endpoint is accepted. +- **Lifecycle:** no linger artifact or command exists. Root explicitly controls `user@1200.service`; the namespace unit cannot start until a separate operator approval marker exists. +- **Filesystem:** the complete user manager receives a private read-only `/srv` with only `/srv/le-app-codex` rebound, empty private `/var`, `/mnt`, `/media`, and `/opt`, private `/tmp`, protected homes/processes, explicit production/archive/transitional path tests, and inaccessible rootful sockets/runtime data. +- **Devices/resources:** closed device policy permits only FUSE and TUN. Aggregate CPU, memory, swap, and task limits cover the user manager and descendants. Hard disk-growth enforcement is deferred and is not a Phase 2B installation blocker; these mandatory controls are not a storage quota. +- **IPv4-only:** the namespace receives only `10.200.120.2/30`; namespace-scoped IPv6 is disabled; daemon IPv6/ip6tables are disabled; nftables drops IPv6 from the project interface. +- **Egress:** only DNS to `9.9.9.9`/`149.112.112.112` and public TCP 80/443 are accepted. Nonpublic/reserved IPv4, loopback, link-local/metadata, multicast, all RFC1918 Docker/LAN/WireGuard ranges, direct SMTP, and all other protocols/ports drop. +- **Host isolation:** host-to-project forwarding and unsolicited inbound traffic drop. Project traffic to host private addresses and observed public/NAT address `74.67.173.56` drops. RootlessKit host-loopback and external port publication are disabled and tested negatively. +- **DNS:** the operator-approved Quad9 malware-blocking, DNSSEC-validating IPv4 pair avoids any LAN exception. `/etc/resolv.conf` is rebound read-only for the user manager. The observed `192.168.10.1` router resolver remains denied. +- **Existing nftables:** the verified ruleset has no `le_app_codex` objects. Dedicated tables avoid editing Docker-owned chains. Interface-scoped terminal drops remain effective alongside existing Docker base chains. +- **Inventory:** the root record checksums pass. UID 1200 is inactive, not lingering, has no process/runtime/socket; project ACLs and modes match Phase 1; no project-rooted mounts exist; all 41 Docker networks fall within denied IPv4/IPv6 space. +- **Rollback:** names and paths are Phase 2B-specific; teardown deletes only the project tables, namespace/veth, marker, and manifest-listed files. + +Hard stops remain: changed Docker version/launcher hash, changed inventory, public/NAT address drift, missing approval marker at namespace-start time, pre-existing project tables/namespace, any acceptance failure, or any endpoint other than the single rootless Unix socket. `ProtectProc=invisible` remains an unproven runtime acceptance candidate. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/SHA256SUMS b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/SHA256SUMS new file mode 100644 index 0000000..0e3733e --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/SHA256SUMS @@ -0,0 +1,9 @@ +e6f6da94601927a23d252f32de93e92c99d31fb2d2cbd9b00f2b88224dbb684e docker-networks.json +9a5d343092c8cf473f0e4d06cb6848f1788aa8bc41b1b3fb7412faea7ea7b48c docker-networks.psv +67178aa964dfa23116984dfc5c677f8c7ef8631d5d7de989193a54a620ceaec1 host-network.txt +4695953ecd1d859198d1a5cbb6242d10ba27493ab3b1ffb3f8f77da39fd458e0 nftables-ruleset.txt +0a0db228df70f4d15e09f5c96bd0d5ac9952f4300b898266d8963225a9815988 runtime-state.txt +4d5c3252ac4f9f537ce3f69110127f13a88ea424cbc69a46ca75448341e2bae7 runtime-versions.txt +1565a8f5b5f08059ab9df69d78fed1b8190ccf674a9d368a436af63c03b804a8 workspace-acls.txt +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 workspace-mounts.txt +362b311c42fdd35bda6bcc9639403dd0f1dca17bc6469ccc9c86a9580021f046 workspace-tree.psv diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.json b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.json new file mode 100644 index 0000000..93a8a71 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.json @@ -0,0 +1,2590 @@ +[ + { + "Name": "arrmail_backend", + "Id": "5a9988b94efd509f9255934bd2ddd49d33dea6feb7bd9d041af8e2df9dc48132", + "Created": "2025-04-23T11:00:44.739870147-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "10.1.0.0/16", + "Gateway": "10.1.0.1" + }, + { + "Subnet": "fd00:1::/64", + "Gateway": "fd00:1::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": { + "61f7dd7992d22dcc66d0c2fad5bdce55adf152cde20020917ef92203ca84be57": { + "Name": "pixelfed-ma-web", + "EndpointID": "2421883a82a5bea3ce646f9d480e632291ddf7c0b5f83598dcd49995622ffb62", + "MacAddress": "7a:53:73:3e:d2:f9", + "IPv4Address": "10.1.0.3/16", + "IPv6Address": "fd00:1::3/64" + }, + "7f22adb997883d0632d47844596c33a6c227ed6f5ee838ef65ae968f91050568": { + "Name": "nextcloud-ma-app", + "EndpointID": "0cea64085bde85fb0f0e237329bf8db8e3c3e97e5316ca08d9c07ff920033655", + "MacAddress": "52:d8:22:af:df:95", + "IPv4Address": "10.1.0.2/16", + "IPv6Address": "fd00:1::2/64" + }, + "d8adfc786a0ae634c2067d4c1af7efd6b70463060c28a1792278a31c06bea2a3": { + "Name": "navidrome", + "EndpointID": "a74bfb9cb3534d6d870220aad563d5a5dda4ab9afa2d337dd562cbd5287f2ce8", + "MacAddress": "1e:f6:a5:14:a0:78", + "IPv4Address": "10.1.0.6/16", + "IPv6Address": "fd00:1::6/64" + }, + "da9f7ee76a51464f64c25b359e5a3a776c8e960b08b5d477462a913c01f8d2d5": { + "Name": "pixelfed-ma-worker", + "EndpointID": "b40483535013c822e3d6ef289255c6853483ce329638f62c5cd425a667a4e1ca", + "MacAddress": "ce:81:ff:09:70:d6", + "IPv4Address": "10.1.0.4/16", + "IPv6Address": "fd00:1::4/64" + }, + "f4dd3b76a4cdeeafea03c692e4675325f0c627ab39e8b1d363b63796ebfe58f7": { + "Name": "castopod", + "EndpointID": "3b78d0966c1dc90b14890c15a061d033ba281717aeaadc4279d83e5752e364cc", + "MacAddress": "92:2e:93:ab:04:04", + "IPv4Address": "10.1.0.5/16", + "IPv6Address": "fd00:1::5/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.1.0.0/16": { + "IPsInUse": 8, + "DynamicIPsAvailable": 65528 + }, + "fd00:1::/64": { + "IPsInUse": 7, + "DynamicIPsAvailable": 18446744073709551609 + } + } + } + } + }, + { + "Name": "arrmailnetmail_clamav", + "Id": "cfae917b997e5c4ef1e73018171e675c29df1c2436adfec97efea186253c393e", + "Created": "2026-07-12T19:36:40.494197563-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.20.0.0/16", + "Gateway": "172.20.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "c29afd41f98a256832f8985faaad1d5ce89e09fd060a554d24b0636efa28e4ac", + "com.docker.compose.network": "clamav", + "com.docker.compose.project": "arrmailnetmail", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "474d10c5dac9a477cef650275a9ba57a091326f3b65c7c077bbcc6f08ed8bea2": { + "Name": "mailu-antispam", + "EndpointID": "4feb62b788edc281fccf57f060e3d188e1c66ea5b3eb8cc08ab6e12afecdf347", + "MacAddress": "b6:e2:7a:62:b6:86", + "IPv4Address": "172.20.0.3/16", + "IPv6Address": "" + }, + "b0f3b9532a0b1644f882e94a8b630868fafb8ea4a9612eec76093897ed789417": { + "Name": "mailu-antivirus", + "EndpointID": "df2318de26f6ed28dade2b3b776053a743e6742ad98dbe1c711aa9aa2fd559e4", + "MacAddress": "6e:f2:a7:11:7f:5a", + "IPv4Address": "172.20.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.20.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "arrmailnetmail_oletools", + "Id": "d89dcf384c2ad16ec15c7136b55476475998040ca924ae4696a332db5309a219", + "Created": "2026-07-12T19:36:40.511402379-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.21.0.0/16", + "Gateway": "172.21.0.1" + } + ] + }, + "Internal": true, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "712ab444f0e51c1c78c7e5cfd1a7b768148cf685e3d9e19d97625a6565330cfc", + "com.docker.compose.network": "oletools", + "com.docker.compose.project": "arrmailnetmail", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "474d10c5dac9a477cef650275a9ba57a091326f3b65c7c077bbcc6f08ed8bea2": { + "Name": "mailu-antispam", + "EndpointID": "8d34116bbad5ef1a8810b4d0eca0210b31101050fd72ccd01a1bca2b176753ef", + "MacAddress": "32:2b:8c:47:5b:33", + "IPv4Address": "172.21.0.3/16", + "IPv6Address": "" + }, + "641e887815b7cfdfd5fe43b2c343ab1ff1d464b963aad70c5267cd02c6f64a52": { + "Name": "mailu-oletools", + "EndpointID": "b78c9d824ca53904cf799570607105da92d4baeede0755db48b829460a9996a2", + "MacAddress": "5a:e8:4d:56:f0:6c", + "IPv4Address": "172.21.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.21.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "arrmailnetmail_radicale", + "Id": "b7976abb6e559f820b107cc9c5f1ff266c2f3462eff1543a6f2610cb32782636", + "Created": "2026-07-12T19:36:40.46738641-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.18.0.0/16", + "Gateway": "172.18.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "4e1881b561a34fe5316d46cf68730c5258eb32df58d925905af3ede8de32600a", + "com.docker.compose.network": "radicale", + "com.docker.compose.project": "arrmailnetmail", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "58f1cabc3d2e10a558e1a7f68a7fdc913126932b5096c36427f6d2531c4e69d2": { + "Name": "mailu-front", + "EndpointID": "e80db670460153de0d68b5e7cb8e960705baf4493ad35252312f057f890974fe", + "MacAddress": "2a:94:8f:32:d6:bf", + "IPv4Address": "172.18.0.3/16", + "IPv6Address": "" + }, + "780d6c1381d3134151044b62775928e6cac92d29dd56ed5d6eb4580f0d0c4815": { + "Name": "mailu-webdav", + "EndpointID": "34ee1fae89ba404557f50c052281d235584c5e47381f2b8e54c23b5847fe1351", + "MacAddress": "06:e3:a0:1b:d7:25", + "IPv4Address": "172.18.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.18.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "arrmailnetmail_tika", + "Id": "ecba5adea6ea9541e297b2cd4d359779750c1377b72ff2eb82e6106aed7727f5", + "Created": "2026-07-12T19:36:40.483760625-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.19.0.0/16", + "Gateway": "172.19.0.1" + } + ] + }, + "Internal": true, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "e9184dbd1edfc67590c0189bfec4e8843c85d0818e1ee26f93fbca72517d20bc", + "com.docker.compose.network": "tika", + "com.docker.compose.project": "arrmailnetmail", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "0c05d8f118f392abdf5846b7dcd1dcc0a7f0215cf54c5ddf710d3218c9bc1a2b": { + "Name": "mailu-tika", + "EndpointID": "df2269d64edd34e810338b2cfc6bc6a80e87db02d92140fd4287df5a83b46e49", + "MacAddress": "1e:23:07:19:59:f6", + "IPv4Address": "172.19.0.2/16", + "IPv6Address": "" + }, + "1471816b07bdc01223f3ab27a67eb82054bdf9b73d637bb2687ee8b68ed7b1e1": { + "Name": "mailu-imap", + "EndpointID": "470729375c74f8c16e73f5ad29e1a84c305478d52cdb9689aad65deaf53ae9ae", + "MacAddress": "16:de:00:7d:a0:c4", + "IPv4Address": "172.19.0.3/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.19.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "backend_net", + "Id": "e02b7c89a2d3b2d9e51899fbca6bdae8cf1dcbfdde3ac800cfc826bf3fdd9219", + "Created": "2025-08-14T19:48:53.901407512-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "10.99.0.0/16", + "Gateway": "10.99.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": { + "060d9803758e763a8df4fe0663422a64205e30eaff48084fd2ed4458df3d7998": { + "Name": "postgres_shared", + "EndpointID": "582d81ff62b8f93409eaa16fdf76f9a3288e9c8fc6b4be698e90bdc58fe87a1c", + "MacAddress": "ee:3d:bf:85:ad:71", + "IPv4Address": "10.99.0.3/16", + "IPv6Address": "" + }, + "3c46a93c75cb2f5357254b147ec1e898f0e5f6f184c9c98e4826b4b5fa4a8019": { + "Name": "forgejo", + "EndpointID": "cc69c0be647ede8ffe52ad2f963fc9281eeb0dff237e6a75bb8fdbfe4fa08eff", + "MacAddress": "12:03:81:7b:a3:a0", + "IPv4Address": "10.99.0.7/16", + "IPv6Address": "" + }, + "50f1cb3d67ead6a4f3c72ff86cd3b8abfea51370dccc3e28b802a95db1fcbdda": { + "Name": "misskey", + "EndpointID": "cfa876f57100d88c1e90c62e1b9e189a1e93e26dda04a2f14d5783a22971aaae", + "MacAddress": "62:40:14:75:0c:54", + "IPv4Address": "10.99.0.6/16", + "IPv6Address": "" + }, + "61f7dd7992d22dcc66d0c2fad5bdce55adf152cde20020917ef92203ca84be57": { + "Name": "pixelfed-ma-web", + "EndpointID": "d7e8cb6aafa2c41ac34a6a46dc2009b23d022e27aadf0267b959c21bafca761b", + "MacAddress": "7a:d9:35:54:8b:79", + "IPv4Address": "10.99.0.13/16", + "IPv6Address": "" + }, + "6e5e014d6402dc8d961f18591c86a81419b915c859bc3893dba91b1d39ad0868": { + "Name": "mastodon-streaming", + "EndpointID": "8cc59a18b78195b2eb0d9deaa3804ffb91d0a96abfd51a4d523d2fb31c009b51", + "MacAddress": "9e:b0:7e:b3:51:44", + "IPv4Address": "10.99.0.11/16", + "IPv6Address": "" + }, + "7833a50c0c298965bb4a2bb03fdab227f3ea8c19570285c6fcab5838cf5120bc": { + "Name": "lemmy", + "EndpointID": "048999796dda141f72f67fb14d73bc19fa11390690afa15440152e4cc1f7fd7f", + "MacAddress": "42:4b:f7:0a:85:52", + "IPv4Address": "10.99.0.9/16", + "IPv6Address": "" + }, + "7f22adb997883d0632d47844596c33a6c227ed6f5ee838ef65ae968f91050568": { + "Name": "nextcloud-ma-app", + "EndpointID": "203e94d3744391aa5529023a5907a23b5b2ec7031049073b87a6d00e0bdfa11d", + "MacAddress": "72:87:98:78:e0:0c", + "IPv4Address": "10.99.0.5/16", + "IPv6Address": "" + }, + "83018ba622024d3f29c8258baff1a6583a6179851752f09abd7d52066fac9f12": { + "Name": "mysql_shared", + "EndpointID": "8c4ffc79941e2e19600c0616588776876de3ca1a693177d5aac7beab1890b083", + "MacAddress": "8a:66:9e:4c:23:c7", + "IPv4Address": "10.99.0.2/16", + "IPv6Address": "" + }, + "88383ca7ad1540e8ccd9d4328a84917b0059dafb83fdfa74b6cad024e7321a21": { + "Name": "picsur", + "EndpointID": "427ce7d5fdb397121a488ba2d73515dd07ef8240815f0c01d8d5b7f6869acf44", + "MacAddress": "ee:98:e1:9d:a8:36", + "IPv4Address": "10.99.0.8/16", + "IPv6Address": "" + }, + "a372d2c6524fa3e791c04f299ee61de6bcccaaa278033cc8f38c51aa75ccbfaf": { + "Name": "mastodon-web", + "EndpointID": "ba6437453a8e4201a4bc42139f342883da84634f9456cb44ae9222202ed6914d", + "MacAddress": "32:52:44:61:b8:4a", + "IPv4Address": "10.99.0.10/16", + "IPv6Address": "" + }, + "abb10d266b9ab1b51ff231e0d119d927159ff6b674a22845040016009c06f8fe": { + "Name": "immich_machine_learning", + "EndpointID": "9cd98cf902bfc6394b50e26e36e4d95669b9c671e4249d06ac0705eb8bd9657c", + "MacAddress": "d6:f1:e2:17:08:78", + "IPv4Address": "10.99.0.18/16", + "IPv6Address": "" + }, + "d8adfc786a0ae634c2067d4c1af7efd6b70463060c28a1792278a31c06bea2a3": { + "Name": "navidrome", + "EndpointID": "7489fda6c409027e10a60a7772ebcc60e009223e9569f648cc2d7066afac4d84", + "MacAddress": "fa:e9:68:f6:60:76", + "IPv4Address": "10.99.0.17/16", + "IPv6Address": "" + }, + "da9f7ee76a51464f64c25b359e5a3a776c8e960b08b5d477462a913c01f8d2d5": { + "Name": "pixelfed-ma-worker", + "EndpointID": "9977204db9a73900fb221b7362a91ef5ee0422de7d4cc3a81d079abfb34697d6", + "MacAddress": "ee:9a:12:2b:4a:4e", + "IPv4Address": "10.99.0.14/16", + "IPv6Address": "" + }, + "e56d4d8aee10347961749afd8d9ade62a14991662b4b31512fe2b8c53176b9df": { + "Name": "mastodon-sidekiq", + "EndpointID": "e0adad95ec12d46440d85365886f9bf193ffb6fa092460a78e1369b305c669e4", + "MacAddress": "66:55:47:40:6b:5b", + "IPv4Address": "10.99.0.12/16", + "IPv6Address": "" + }, + "f4dd3b76a4cdeeafea03c692e4675325f0c627ab39e8b1d363b63796ebfe58f7": { + "Name": "castopod", + "EndpointID": "d97d01248894b9fab2037b7c7c500e59bd211e2d0b7e3cadb413551d8c67062e", + "MacAddress": "52:b8:8c:34:de:21", + "IPv4Address": "10.99.0.15/16", + "IPv6Address": "" + }, + "f84b2b792761fa12cd841e4dcf3473f7ccfc017de6ce9131835b6a6c0bdf9542": { + "Name": "peertube", + "EndpointID": "6534c9171e21786f95962a4b35f256a95e83a271db398b378536960a50a994b8", + "MacAddress": "42:45:2f:1f:e5:9e", + "IPv4Address": "10.99.0.16/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.99.0.0/16": { + "IPsInUse": 19, + "DynamicIPsAvailable": 65517 + } + } + } + } + }, + { + "Name": "bridge", + "Id": "a13cb8d340762b2b1d65b819b4d9fc189843c413e9f9bc10b88e19aecf908a5e", + "Created": "2026-07-12T19:18:03.400709764-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.17.0.0/16", + "Gateway": "172.17.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + }, + "Labels": {}, + "Containers": { + "2ee8f491397b5cbab595121f0e5e57fa5778329090d27a80b002bdd83a5ee8f2": { + "Name": "le-payload-pg", + "EndpointID": "24ced074723321e07dd5e21fde6f4f97108677aff8f4b02c6be94107c685b9e2", + "MacAddress": "76:51:09:bc:ee:36", + "IPv4Address": "172.17.0.4/16", + "IPv6Address": "" + }, + "6adbd503febfd8a6236fc7c941df7bfc748e76dcd510c11e69a3ff02776ff344": { + "Name": "le-directus-source", + "EndpointID": "ae5dac68039502677b1423b8fb11fa274670229d26234199c485dc1f0b97312d", + "MacAddress": "06:b7:cb:5f:ee:b5", + "IPv4Address": "172.17.0.3/16", + "IPv6Address": "" + }, + "6b0affe65f5e3589baa0d57169713a1d9ffa5df5c9f6484a8e32bc4070d67ee6": { + "Name": "buildx_buildkit_defaultbuilder0", + "EndpointID": "47fb610adb7ef59ed80c1ac339cc468a22b339470b507dffa87a00728730fcd2", + "MacAddress": "4e:53:8e:e3:f3:97", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.17.0.0/16": { + "IPsInUse": 6, + "DynamicIPsAvailable": 65530 + } + } + } + } + }, + { + "Name": "castopod_ma_net", + "Id": "051bbcfc21d81c36da63074a661a6aa7a555f476ce0362312d772817c0645e03", + "Created": "2026-07-12T19:37:03.407062294-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.41.0.0/16", + "Gateway": "10.41.0.1" + }, + { + "Subnet": "fd00:41::/64", + "Gateway": "fd00:41::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "5b82f00a12081e56cbcfac99dd587b15d6117108e783ca7ff2a96aa2003d092e", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiopodcast", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "acfc590699257e1c049c061c83e83987151f90b965cd9b57bc36173ea11ca22b": { + "Name": "castopod-redis", + "EndpointID": "dbacfd7b0318eaa998e62be5adb2fcb798da5d0725aaf9c1f749865732d27233", + "MacAddress": "0e:fd:71:63:c7:14", + "IPv4Address": "10.41.0.3/16", + "IPv6Address": "fd00:41::3/64" + }, + "f4dd3b76a4cdeeafea03c692e4675325f0c627ab39e8b1d363b63796ebfe58f7": { + "Name": "castopod", + "EndpointID": "a400911130eab8b6ce0f05d5da08e121514962fb3e11d7d7b6ce4b8f3dd351ec", + "MacAddress": "1a:26:94:2c:54:d5", + "IPv4Address": "10.41.0.2/16", + "IPv6Address": "fd00:41::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.41.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + }, + "fd00:41::/64": { + "IPsInUse": 4, + "DynamicIPsAvailable": 18446744073709551612 + } + } + } + } + }, + { + "Name": "collabora_s6_net", + "Id": "28584d053169444c5801b7fa3bb3c85c5e8659d9deba3d2bcde5d04cf12538bf", + "Created": "2026-07-12T19:37:29.005953186-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.17.0.0/16", + "Gateway": "10.17.0.1" + }, + { + "Subnet": "fd00:17::/64", + "Gateway": "fd00:17::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "feff332403f00ccc4a2154ff121099fcc647fb0aaa7e718c64868ef192dfb748", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetoffice", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "e467858b160d2106cb37ed77c1ec28f0f2b8017d0ef347813c9c4015b31bea89": { + "Name": "collabora", + "EndpointID": "77ffa42f0455ccb73b305101073d66960e68375aa58c9456069d6b3b369fe4be", + "MacAddress": "ca:76:4d:e4:bf:ed", + "IPv4Address": "10.17.0.2/16", + "IPv6Address": "fd00:17::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.17.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:17::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "conduit_net", + "Id": "263c8508de6fd8a2831fbd935e308d22b9340e014ee170739ce13ea7b88c0e46", + "Created": "2026-07-12T19:37:01.115080742-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.26.0.0/16", + "Gateway": "10.26.0.1" + }, + { + "Subnet": "fd00:26::/64", + "Gateway": "fd00:26::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "d9d28efadf9d31570cb249c9875db287b0bd0105e36ed6de505c0e7ff2e0aabf", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiomatrix", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "8ab6ba916936d9ea394014a5a08c60e9abbd6aeb3873e1145840e50dddae6080": { + "Name": "continuwuity", + "EndpointID": "cbd826246061ea7ae621acbb5dc64988e5bda56d161a2153efd2b4c7a3d97071", + "MacAddress": "6a:5d:bc:2a:0d:84", + "IPv4Address": "10.26.0.2/16", + "IPv6Address": "fd00:26::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.26.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:26::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "directus_net", + "Id": "8b881e3673225da1d9fe2356169376462519ce85982f1fc250ba23358b09ffe5", + "Created": "2026-07-12T19:36:41.500540602-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.40.0.0/16", + "Gateway": "10.40.0.1" + }, + { + "Subnet": "fd00:40::/64", + "Gateway": "fd00:40::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "ddbe1da38417f5521047f931fd7cedd5aae970773bedb51f8d06c76023b6bac7", + "com.docker.compose.network": "default", + "com.docker.compose.project": "lasereverythingnetforms", + "com.docker.compose.version": "5.3.1" + }, + "Containers": {}, + "Status": { + "IPAM": { + "Subnets": { + "10.40.0.0/16": { + "IPsInUse": 3, + "DynamicIPsAvailable": 65533 + }, + "fd00:40::/64": { + "IPsInUse": 2, + "DynamicIPsAvailable": 18446744073709551614 + } + } + } + } + }, + { + "Name": "forge_net", + "Id": "87e08370adff24049790aeedbe393d47ce280cf8f649c1f8fcaf9088fbb4dd26", + "Created": "2026-07-12T19:36:48.725225769-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.27.0.0/16", + "Gateway": "10.27.0.1" + }, + { + "Subnet": "fd00:27::/64", + "Gateway": "fd00:27::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "399b99dd21397b2414ff26e477688df786b85c4f16ba0adb893ab7cb0612e6c0", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyioforge", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "3c46a93c75cb2f5357254b147ec1e898f0e5f6f184c9c98e4826b4b5fa4a8019": { + "Name": "forgejo", + "EndpointID": "b549e2bbb49b02d336f7244b2ca4099aaf31bd355abbd8507c185eb30d5cce39", + "MacAddress": "4a:32:cc:25:33:6c", + "IPv4Address": "10.27.0.2/16", + "IPv6Address": "fd00:27::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.27.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:27::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "heimdall_s6_net", + "Id": "bca9ecea74f16d8044d2d0a30d51d206e657095c0b13031561a3e4a04733ae23", + "Created": "2026-07-12T19:37:05.400305432-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.10.0.0/16", + "Gateway": "10.10.0.1" + }, + { + "Subnet": "fd00:10::/64", + "Gateway": "fd00:10::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "15ce1785540a68d18ba3a86a6f7a48726b28dba92b2047cfae870f538bac8e87", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinet", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "28434729c2d4368168aa7e4ee73798d5945f6f276290d45967fa707cdf677f85": { + "Name": "heimdall", + "EndpointID": "315b129c37e6f716bc6e861cefb9b5bcfd7793e025d362635bf695afc572df83", + "MacAddress": "22:50:05:f4:fa:15", + "IPv4Address": "10.10.0.2/16", + "IPv6Address": "fd00:10::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.10.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:10::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "host", + "Id": "d905d3d625d964d04f60fe6d990fdbff25ffab369291a39b5fc86338416be72e", + "Created": "2025-03-10T13:01:40.969884714-04:00", + "Scope": "local", + "Driver": "host", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": null + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": {}, + "Status": { + "IPAM": {} + } + }, + { + "Name": "immich_s6_net", + "Id": "4ecbee63f4b0f705479ec65b74b57c618b61d76da91bc5c042717460709dadb8", + "Created": "2026-07-12T19:37:29.380624323-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.18.0.0/16", + "Gateway": "10.18.0.1" + }, + { + "Subnet": "fd00:18::/64", + "Gateway": "fd00:18::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "0f9b0c953362dbe4260f8afa14834a5837e21d41a2710868313c5584b172388d", + "com.docker.compose.network": "default", + "com.docker.compose.project": "immich", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "2c7163672e538be6f55d4c5fe470a8393ed1b1659438dfc85428faaa7b9a3a5e": { + "Name": "immich_redis", + "EndpointID": "ab97f097a229681e9cd55a945ee53839169754f54669f3128b0fc455302ba986", + "MacAddress": "1a:2c:0d:d9:bc:88", + "IPv4Address": "10.18.0.2/16", + "IPv6Address": "fd00:18::2/64" + }, + "abb10d266b9ab1b51ff231e0d119d927159ff6b674a22845040016009c06f8fe": { + "Name": "immich_machine_learning", + "EndpointID": "96d7b0581a4727894e23075e71b82a36c92e896ff766cb665887544f81f13ead", + "MacAddress": "fa:ec:2a:27:51:f0", + "IPv4Address": "10.18.0.4/16", + "IPv6Address": "fd00:18::4/64" + }, + "fa5b06636e9f54e46c825aa41474be316514dde187ac93bb367ec5332ed90cb3": { + "Name": "immich_postgres", + "EndpointID": "5f1d43f9ef3c740843e437af07bf03af8b862fba76b553aff190d03f96d972ae", + "MacAddress": "1a:03:54:25:85:58", + "IPv4Address": "10.18.0.3/16", + "IPv6Address": "fd00:18::3/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.18.0.0/16": { + "IPsInUse": 6, + "DynamicIPsAvailable": 65530 + }, + "fd00:18::/64": { + "IPsInUse": 5, + "DynamicIPsAvailable": 18446744073709551611 + } + } + } + } + }, + { + "Name": "irc_net", + "Id": "527a7cd84ff6870c21827f3783eb75e6f8262308f9ee598e112a273aaa7f3118", + "Created": "2026-07-12T19:36:59.855430168-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.20.0.0/16", + "Gateway": "10.20.0.1" + }, + { + "Subnet": "fd00:20::/64", + "Gateway": "fd00:20::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "18f5e6db87718f8f1a672d2aacbdf98e94a1c8994057ee30576d99089ee70b54", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyioirc", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "afd4982b1ed279fd96c61d17a457b7e77c7d7ce4d5d8039daad2a739cd706c27": { + "Name": "ergo", + "EndpointID": "1aaf487a50dd4e4fec01402bae281d523e5e87dedb56b57f209a787a1c898a19", + "MacAddress": "4a:e0:51:b2:42:5c", + "IPv4Address": "10.20.0.2/16", + "IPv6Address": "fd00:20::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.20.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:20::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "jellyfin_s6_net", + "Id": "8200b83f03e90a646dba0743d232c77881817a9d3078a713cdc4c2aa23d2396e", + "Created": "2026-07-12T19:37:30.429980231-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.9.0.0/16", + "Gateway": "10.9.0.1" + }, + { + "Subnet": "fd00:9::/64", + "Gateway": "fd00:9::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "ea807b318f12746df770d3396a840bf09343c7063f6998003df1475d37dbb0e0", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetwatch", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "e8191d167515cec8db66581943fbde4cb77f68acaa0f28f9a73e71f008764fb0": { + "Name": "jellyfin", + "EndpointID": "2d5621ac78a43b0db3591ec50abd4535e84a49ed7b56bd16b93293a72e239e5b", + "MacAddress": "da:71:7f:a7:b3:12", + "IPv4Address": "10.9.0.2/16", + "IPv6Address": "fd00:9::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.9.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:9::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "lasereverythingnetmanager_default", + "Id": "6caddd639b0bce3f75495489bb6f4cbd99db74098bf70a3ecf5e23f863a892ea", + "Created": "2026-07-12T19:36:41.750837262-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.22.0.0/16", + "Gateway": "172.22.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "2a53b3669f14106d548aeaee1310ca3c846345ef2a419bfd31438d30d9a9e785", + "com.docker.compose.network": "default", + "com.docker.compose.project": "lasereverythingnetmanager", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "99a1798b8e28514ef8e232727a575f084c30948899d499158a784e6a330da94c": { + "Name": "lasereverything-manager-bus-core", + "EndpointID": "6393af123531adf9acc5ccf74510bb8af4b7afa11b98b526e618682edb99799d", + "MacAddress": "a6:72:df:56:c6:25", + "IPv4Address": "172.22.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.22.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + } + } + } + } + }, + { + "Name": "lasereverythingnettv_default", + "Id": "875fced92cd0b31f327a8e3f72b438fbe64802879a8b2cb425ea9b3cd66e0308", + "Created": "2026-06-30T13:48:55.050744357-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.25.0.0/16", + "Gateway": "172.25.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "138b131e235c8faf1bee6bd2342b76ef1fea2e41aefd2271e308daa57c49ef37", + "com.docker.compose.network": "default", + "com.docker.compose.project": "lasereverythingnettv", + "com.docker.compose.version": "5.1.4" + }, + "Containers": {}, + "Status": { + "IPAM": { + "Subnets": { + "172.25.0.0/16": { + "IPsInUse": 3, + "DynamicIPsAvailable": 65533 + } + } + } + } + }, + { + "Name": "lemmy_net", + "Id": "03ee8c9e4beabc06faa6bae2180f5ed63e02685c70d2960aa52ec977f869aede", + "Created": "2026-07-12T19:37:00.040912179-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.28.0.0/16", + "Gateway": "172.28.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "5bb363ed66768dd5cc65c2d92744abc515d7dab1eb75ec8b76dcec5a6509a8dc", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiolemmy", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "485c75ee0812204c1831aa22ad42d59195a112aeed034ff20dfe81f0cbbb47ca": { + "Name": "lemmy-proxy", + "EndpointID": "c7f2182c5deafa340a9e49dbebfa03d94c8a5d3e110483b137368f7178ca8b0b", + "MacAddress": "96:e4:26:5b:eb:14", + "IPv4Address": "172.28.0.5/16", + "IPv6Address": "" + }, + "7833a50c0c298965bb4a2bb03fdab227f3ea8c19570285c6fcab5838cf5120bc": { + "Name": "lemmy", + "EndpointID": "63ac58d58cd199c56442f721b790a2b17f51bd7c7eb4a71d604fd42806aa0e11", + "MacAddress": "f2:ab:81:ab:74:c7", + "IPv4Address": "172.28.0.3/16", + "IPv6Address": "" + }, + "9bcc93b51d5ed1d9f7eaa13a7ae0aa88a008b2102b80437a9219707e2134f8c6": { + "Name": "lemmy-ui", + "EndpointID": "d95bb5ad4880e15f7dfbcc541cd965fcbd210b2db031f765c67b6898873461b2", + "MacAddress": "82:5e:b7:9e:b4:76", + "IPv4Address": "172.28.0.4/16", + "IPv6Address": "" + }, + "d3d230a95b11c0d3a5eb52072ff898a2750c08fec9caad65e0db3c844abfb4ad": { + "Name": "lemmy-pictrs", + "EndpointID": "c80e6a89242085c49a1ce01a78aa2714718379d761468895b11db241bec72761", + "MacAddress": "f2:7a:74:13:bb:ad", + "IPv4Address": "172.28.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.28.0.0/16": { + "IPsInUse": 7, + "DynamicIPsAvailable": 65529 + } + } + } + } + }, + { + "Name": "mailu_net", + "Id": "68e2ed0ec6ac4ef4297b58bbf5d3f5bc374d324bbbcd7cdd7b5c8a65150bd367", + "Created": "2026-07-12T19:36:40.451484058-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.2.0.0/16", + "Gateway": "10.2.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "f4ae6ebcfafcc1c3bbe4761a132deb08090d005d3e8b9246c40c884c0057cf64", + "com.docker.compose.network": "default", + "com.docker.compose.project": "arrmailnetmail", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "1471816b07bdc01223f3ab27a67eb82054bdf9b73d637bb2687ee8b68ed7b1e1": { + "Name": "mailu-imap", + "EndpointID": "847b6c8d130aaff43f5ee88f83519ed19da4b63902a8209b7f7a8d66237f8fc3", + "MacAddress": "22:02:59:41:eb:0a", + "IPv4Address": "10.2.0.6/16", + "IPv6Address": "" + }, + "474d10c5dac9a477cef650275a9ba57a091326f3b65c7c077bbcc6f08ed8bea2": { + "Name": "mailu-antispam", + "EndpointID": "30dd3ed7555ac30ff290a0560f564057221d9eabcd1d9fceac5b666c1363360b", + "MacAddress": "4a:13:9b:18:15:03", + "IPv4Address": "10.2.0.7/16", + "IPv6Address": "" + }, + "4b2473f63597d6ff90ac713adcffdcd593a0dd7dcdf7148609ff700665998e81": { + "Name": "mailu-fetchmail", + "EndpointID": "8e3fcfd2378afac94e509ddc0c9029e6cdb03344dfd623c59b367eeb2c5a44b7", + "MacAddress": "36:c9:a8:5b:1e:f3", + "IPv4Address": "10.2.0.8/16", + "IPv6Address": "" + }, + "58f1cabc3d2e10a558e1a7f68a7fdc913126932b5096c36427f6d2531c4e69d2": { + "Name": "mailu-front", + "EndpointID": "586828ed32deb2309e41eef2603b50119c8b08cd66ec8880d642e60efc4be423", + "MacAddress": "12:4b:19:cf:32:dc", + "IPv4Address": "10.2.0.4/16", + "IPv6Address": "" + }, + "8e4be39280fd777f26cd1caa0e78158c56c76ee78d79dbc0166a033318031a5e": { + "Name": "mailu-smtp", + "EndpointID": "2df264b00347fe5df227fa5bd4f446806f4f3a09d606f797853c5e5664ab9439", + "MacAddress": "ba:22:63:6f:83:7d", + "IPv4Address": "10.2.0.5/16", + "IPv6Address": "" + }, + "ad0080e530b138e74fafa5a7a894674647b28e2af113158abd4d3c6760e8bcdf": { + "Name": "mailu-admin", + "EndpointID": "b23de1a49199ac6832131e91b0834588b7d9fe51630149980f8406661b95c033", + "MacAddress": "0a:9a:b4:cd:35:fa", + "IPv4Address": "10.2.0.3/16", + "IPv6Address": "" + }, + "f1159692a59aa9dca7ae6d304d179cb9bf7da41e872239ef8c1fdb2d6a546376": { + "Name": "mailu-resolver", + "EndpointID": "5a3a7387fe4920db161017c619b3be14b65ec1567989e545da11882f60fa1332", + "MacAddress": "b6:9f:5c:74:c0:3f", + "IPv4Address": "10.2.255.254/16", + "IPv6Address": "" + }, + "fe292d43b40561dc065754e7c059638cddc340798ac3ae21fae295a4888ba954": { + "Name": "mailu-redis", + "EndpointID": "bc704dfce77ccc2062c385c584d3130b373292e43358b860c5ab63b36b3cdcb7", + "MacAddress": "52:3b:00:6c:ad:4d", + "IPv4Address": "10.2.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.2.0.0/16": { + "IPsInUse": 11, + "DynamicIPsAvailable": 65525 + } + } + } + } + }, + { + "Name": "makearmy_net", + "Id": "11ceaf8736f513afa23ead91ff9bafe8ccd158b4233152857c0ef5dad3d9cf91", + "Created": "2025-10-16T10:10:14.688213369-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "10.55.0.0/16", + "Gateway": "10.55.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": { + "2d7d7a1250b29f5e66c3068a4077e2d3df801de3f0c1802882aae1e39a84b05e": { + "Name": "bgbye", + "EndpointID": "f0a27e2fa3818e45e2c0d78220db264b00460fb77a6aa2caa1288e4b7450f89a", + "MacAddress": "62:6c:06:27:f0:21", + "IPv4Address": "10.55.0.3/16", + "IPv6Address": "" + }, + "3c10dda3815a2c0b0c494fb99a198a331b85c61f0b27ed1537bd2743355fd363": { + "Name": "makearmy-app", + "EndpointID": "35a395a4a51f8784f0fa579edbf4d8b2a043f5120dab48b5208f360fb11afa31", + "MacAddress": "c6:88:06:e1:21:66", + "IPv4Address": "10.55.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.55.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "makearmyiocast_default", + "Id": "2d50ca7a8f5d486c502631c656125fe63c50397e373ebd2bda3712c9a8a35a18", + "Created": "2026-07-12T19:36:41.937640105-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.23.0.0/16", + "Gateway": "172.23.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "a19f300d941ba950f8bc2c4e4c3b5dbd959db1c88d595b48195f21c4d14e5a3c", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiocast", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "f1b1792061575857d980a82ab8fe0c153fcedd1e1c4f0cd48f2039cab1860728": { + "Name": "owncast", + "EndpointID": "ae63e8789ff4d6225ae85e20814b9ec8f041d8b4afedad99466feebdd41917f9", + "MacAddress": "82:6a:c9:03:b9:bb", + "IPv4Address": "172.23.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.23.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + } + } + } + } + }, + { + "Name": "makearmyiodcm_default", + "Id": "eeee48934935bfcf2616d860f24a5167024cf1e647b57954f6f0287ae3ef90f7", + "Created": "2026-07-12T19:36:42.679213098-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.24.0.0/16", + "Gateway": "172.24.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "1e69821bb1f946c5c442914edf3250592ea32494ce2648028510db2fa175cd3f", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiodcm", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "11b7e889e813851513bbeb8913505a45e280669f2b8d2b79384e804ff7c75751": { + "Name": "dcm", + "EndpointID": "29e82a762316350de29abf8804fdc773ece9c73a1a48575241338d93fb4ef8e8", + "MacAddress": "b2:f5:f7:e1:b7:d2", + "IPv4Address": "172.24.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.24.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + } + } + } + } + }, + { + "Name": "makearmyiovdo_default", + "Id": "dee363d4e0c687ab99d4e02614824d8e8106fe4337ac8a56009918dc7854d704", + "Created": "2026-06-30T13:55:34.614261972-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.29.0.0/16", + "Gateway": "172.29.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "3432138540de749934fedf38912abfe27f493268e2a81425c20ade17859e4e71", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiovdo", + "com.docker.compose.version": "5.1.4" + }, + "Containers": {}, + "Status": { + "IPAM": { + "Subnets": { + "172.29.0.0/16": { + "IPsInUse": 3, + "DynamicIPsAvailable": 65533 + } + } + } + } + }, + { + "Name": "makearmyiowatch_peertube_net", + "Id": "4efe811b8402d7bac5339bf03af15753dc54ba016cb556f38b744407c939bcca", + "Created": "2026-07-12T19:37:04.335177431-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.27.0.0/16", + "Gateway": "172.27.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "2383d27b354a647cf3cf3a9b0e64ce1602988965647ceaaff2036b14acad5180", + "com.docker.compose.network": "peertube_net", + "com.docker.compose.project": "makearmyiowatch", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "2064a6b872b943793577e434d08e72b6b868965429bbb2d6aee0f5542d237a1a": { + "Name": "peertube-redis", + "EndpointID": "d6638f3ca475ca448dd96d59b2e12e14052e7574e39c310cc582650240606b7f", + "MacAddress": "76:6b:67:ce:d7:1f", + "IPv4Address": "172.27.0.2/16", + "IPv6Address": "" + }, + "f84b2b792761fa12cd841e4dcf3473f7ccfc017de6ce9131835b6a6c0bdf9542": { + "Name": "peertube", + "EndpointID": "d6747fd538c805a275911903504ea81977e0110fa71e99013a2bdaf154228418", + "MacAddress": "c2:ad:3e:1a:a3:03", + "IPv4Address": "172.27.0.3/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.27.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "mastodon_net", + "Id": "f3f5c7cfa74eee61b9b51827180e94e41d4bd45d0f42299fd6f8a010d2ac0e09", + "Created": "2026-07-12T19:37:00.530814036-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.31.0.0/16", + "Gateway": "10.31.0.1" + }, + { + "Subnet": "fd00:31::/64", + "Gateway": "fd00:31::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "d50d37ab014a29be745bf683fd37f05165f74fda42ffd1e3fa9226f69e923c28", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiomastodon", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "02375df49d37102b10ac8962dcfa2b3835c298f3f0994f93a1aa87411aca81f8": { + "Name": "mastodon-es", + "EndpointID": "5fea6cddd1195affac6cdc6b3b416eaf582f90f913e049fb53cd9bce650a0b85", + "MacAddress": "82:3a:e2:10:44:7b", + "IPv4Address": "10.31.0.2/16", + "IPv6Address": "fd00:31::2/64" + }, + "6e5e014d6402dc8d961f18591c86a81419b915c859bc3893dba91b1d39ad0868": { + "Name": "mastodon-streaming", + "EndpointID": "88a2627390d3ee6c656090ef8c83481e337ebeed523a58fdcc5665fc98a62735", + "MacAddress": "aa:30:65:16:a2:03", + "IPv4Address": "10.31.0.5/16", + "IPv6Address": "fd00:31::5/64" + }, + "a372d2c6524fa3e791c04f299ee61de6bcccaaa278033cc8f38c51aa75ccbfaf": { + "Name": "mastodon-web", + "EndpointID": "104726fa402ef5474ac87a38ae29cebf691e99c586ea65bf679da6e796939238", + "MacAddress": "0e:4e:31:5e:4b:83", + "IPv4Address": "10.31.0.4/16", + "IPv6Address": "fd00:31::4/64" + }, + "b2c344287cb2df01cd8429a8603ff61ae0f71c0c5853af0f8b138c98e23bd197": { + "Name": "mastodon-redis", + "EndpointID": "2c8d8b8b3a70cfe97f70bf2921e2bfd1e2d4ba403448e3f0367cdb686f6cf1c3", + "MacAddress": "e6:db:0a:4f:e6:d7", + "IPv4Address": "10.31.0.3/16", + "IPv6Address": "fd00:31::3/64" + }, + "e56d4d8aee10347961749afd8d9ade62a14991662b4b31512fe2b8c53176b9df": { + "Name": "mastodon-sidekiq", + "EndpointID": "1bce1d048f508f5e1c4e6e0192373d58e2d9c3e670a683b2e7235940068e0ee4", + "MacAddress": "26:8c:93:19:3c:46", + "IPv4Address": "10.31.0.6/16", + "IPv6Address": "fd00:31::6/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.31.0.0/16": { + "IPsInUse": 8, + "DynamicIPsAvailable": 65528 + }, + "fd00:31::/64": { + "IPsInUse": 7, + "DynamicIPsAvailable": 18446744073709551609 + } + } + } + } + }, + { + "Name": "meet_net", + "Id": "fea0382ed10bb2ed4562cbe1c9c05403c3bb5a08dc30f4f541718438e8689d04", + "Created": "2026-07-12T19:37:01.38348794-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.26.0.0/16", + "Gateway": "172.26.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "93f38ab08b5561ab9fd10ea6cf6f7a35ebeed42d21bf1d11728c9f407dc54082", + "com.docker.compose.network": "meet_net", + "com.docker.compose.project": "makearmyiomeet", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "61a29b21b676f0c208f47bfcbed34a0362e92e102fd17fbf7d1c93ae57ef5f98": { + "Name": "makearmyiomeet-jibri-1", + "EndpointID": "5b843fd9db030d8f714c6c6c1331174a59c3527640c2a4275533b3747ebd4c0b", + "MacAddress": "3a:38:92:07:73:92", + "IPv4Address": "172.26.0.6/16", + "IPv6Address": "" + }, + "7935861697b26386f95f4ed31797cdc55057e20e33b8ab02edc9679ad4fa7710": { + "Name": "makearmyiomeet-prosody-1", + "EndpointID": "2741b7cdea15d8df3f2de9a78b03cf0997a2e89b92efd2142c1aedef1b24c0ea", + "MacAddress": "62:25:b1:72:0a:51", + "IPv4Address": "172.26.0.2/16", + "IPv6Address": "" + }, + "8b4dd9b28573b112b4e9ab7763cda9b3c90278fdd1b9a7bdf302962ccbb4e110": { + "Name": "makearmyiomeet-web-1", + "EndpointID": "40a4fc0c36e76ba4a0cc6d32e96156feb345a310260c019215169e3d172f4965", + "MacAddress": "b6:8b:61:d7:5e:63", + "IPv4Address": "172.26.0.5/16", + "IPv6Address": "" + }, + "c789ccfd3e329e969d4c07cb7725f32a75f4e59eda8ad43ba17099fac5225b19": { + "Name": "makearmyiomeet-jicofo-1", + "EndpointID": "34178af714e568f3a678764c98188ca94c61e1c977ea42091f0ae8c5f864bb75", + "MacAddress": "06:41:f2:e2:af:d1", + "IPv4Address": "172.26.0.3/16", + "IPv6Address": "" + }, + "d775fb0bbbfe68dea9e7dc608606f991d86f66fcb097160cb534041dea9aff32": { + "Name": "makearmyiomeet-jvb-1", + "EndpointID": "7987ed7c614ae72fbfd3ced76cfabb6667e0dce5495fd168bcb721549ae59952", + "MacAddress": "12:2f:88:ec:d9:0a", + "IPv4Address": "172.26.0.4/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.26.0.0/16": { + "IPsInUse": 8, + "DynamicIPsAvailable": 65528 + } + } + } + } + }, + { + "Name": "misskey_net", + "Id": "f10b040acc46cb998d4e010a6d314879b0b1372924552e559cdc440a72fc8511", + "Created": "2026-07-12T19:36:42.851955514-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.29.0.0/16", + "Gateway": "10.29.0.1" + }, + { + "Subnet": "fd00:29::/64", + "Gateway": "fd00:29::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "980854d4f4298da4ca1cda29aa4f505b5d0ce86eed1db01efdaaecd5ed281fc6", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiodeck", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "50f1cb3d67ead6a4f3c72ff86cd3b8abfea51370dccc3e28b802a95db1fcbdda": { + "Name": "misskey", + "EndpointID": "4323a086c9b0c1f24cee7e54ef5af5f4d5d2745c5dee675eb1a306125173fca9", + "MacAddress": "f6:e4:a4:ec:2a:7a", + "IPv4Address": "10.29.0.3/16", + "IPv6Address": "fd00:29::3/64" + }, + "9656d6c5b6619fbaf7ac2e9664b4c4f3885791977232b6d52fbbb90af5af615f": { + "Name": "misskey-redis", + "EndpointID": "fdc9162754da9c517765a12fcd89d961992ed166ce07c29342b5c80496ab76b8", + "MacAddress": "0a:66:a9:39:c6:c3", + "IPv4Address": "10.29.0.2/16", + "IPv6Address": "fd00:29::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.29.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + }, + "fd00:29::/64": { + "IPsInUse": 4, + "DynamicIPsAvailable": 18446744073709551612 + } + } + } + } + }, + { + "Name": "navidrome_s6_net", + "Id": "618855db6757dd36f5ce1d7a57860c17784fd037f1d9bda51a10649f0c62250b", + "Created": "2026-07-12T19:37:28.38002932-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.16.0.0/16", + "Gateway": "10.16.0.1" + }, + { + "Subnet": "fd00:16::/64", + "Gateway": "fd00:16::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "7583ea7cf3d19a85135426a3804b9955675abd5eee26dab56144205be6ca22b1", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetmusic", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "d8adfc786a0ae634c2067d4c1af7efd6b70463060c28a1792278a31c06bea2a3": { + "Name": "navidrome", + "EndpointID": "f07ad57c5753284f6ccf6c96a55409b0567da3159fcb5a7b2ad416714a749245", + "MacAddress": "ea:3c:87:a5:89:e5", + "IPv4Address": "10.16.0.2/16", + "IPv6Address": "fd00:16::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.16.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:16::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "nextcloud_ma_net", + "Id": "dc41f9983a471306cbbfe1a40d3dd62fbeac9bf0d880f5f4405f951500bb656a", + "Created": "2026-07-12T19:36:42.138531906-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.30.0.0/16", + "Gateway": "10.30.0.1" + }, + { + "Subnet": "fd00:30::/64", + "Gateway": "fd00:30::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "2eca22170e08f0af24baa7db51dcc95bf0264eeb5ce4c6b7069c76dc302cb80d", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiocloud", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "7f22adb997883d0632d47844596c33a6c227ed6f5ee838ef65ae968f91050568": { + "Name": "nextcloud-ma-app", + "EndpointID": "be2f4f15194f050bed54b2f7170ffbe501f49a7b073a73488869beead910ad60", + "MacAddress": "2e:6e:b1:47:e3:b5", + "IPv4Address": "10.30.0.3/16", + "IPv6Address": "fd00:30::3/64" + }, + "ec1a1216eaf2ea4ed53f11548de310a74ea38fcaa39ecadb9d2f5ba53f4a9383": { + "Name": "nextcloud-ma-web", + "EndpointID": "3cf431f30d10e44382ee2c8a1fdcb65743e74d52c78215c438aa558f73ceebd8", + "MacAddress": "42:28:5e:bc:39:19", + "IPv4Address": "10.30.0.4/16", + "IPv6Address": "fd00:30::4/64" + }, + "f2706d1280c6506c96e9c833b33eb37501896d3c58a628972e78a33ff719be22": { + "Name": "nextcloud-ma-redis", + "EndpointID": "71a4ff5bb3705f37e38aac519d8070925b28c24e31fc1ffdf2bfbce555bf2e91", + "MacAddress": "d6:e9:95:06:e1:a6", + "IPv4Address": "10.30.0.2/16", + "IPv6Address": "fd00:30::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.30.0.0/16": { + "IPsInUse": 6, + "DynamicIPsAvailable": 65530 + }, + "fd00:30::/64": { + "IPsInUse": 5, + "DynamicIPsAvailable": 18446744073709551611 + } + } + } + } + }, + { + "Name": "none", + "Id": "57b7955ca01df6d18a022258740cf74910ef3c721271f6717e40f60de5b43d0b", + "Created": "2025-03-10T13:01:40.96731816-04:00", + "Scope": "local", + "Driver": "null", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": null + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": {}, + "Status": { + "IPAM": {} + } + }, + { + "Name": "picsur_net", + "Id": "ed1bb1cc9d395d2765dd1d5237d31636383f301802a19a0cc17a21bddcedd8ce", + "Created": "2026-07-12T19:36:49.055595015-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.28.0.0/16", + "Gateway": "10.28.0.1" + }, + { + "Subnet": "fd00:28::/64", + "Gateway": "fd00:28::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "10a028bcbfdefa8d20d6324c700f79e1d3463370503297881c7cc9f21fcaa86e", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyioimages", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "88383ca7ad1540e8ccd9d4328a84917b0059dafb83fdfa74b6cad024e7321a21": { + "Name": "picsur", + "EndpointID": "0cef8708d18755b5a290a8e19d8f8d8636c386123e18494c5b7932f976f3c0fa", + "MacAddress": "4a:e6:aa:01:ee:60", + "IPv4Address": "10.28.0.3/16", + "IPv6Address": "fd00:28::3/64" + }, + "a1885d9afd7fecdc9cd497008679fe90d42e22cba25dae0aec1b9c1a77b17067": { + "Name": "picsur-redis", + "EndpointID": "36dde412654f941397d3b2dde2abf4cbb7bf06012f7afad238320c3ab8f17969", + "MacAddress": "1e:88:7a:ae:c8:7d", + "IPv4Address": "10.28.0.2/16", + "IPv6Address": "fd00:28::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.28.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + }, + "fd00:28::/64": { + "IPsInUse": 4, + "DynamicIPsAvailable": 18446744073709551612 + } + } + } + } + }, + { + "Name": "pixelfed_ma_net", + "Id": "59ca07fbf2005a5070facb1d14666dd29946ae11093378e7b98c43c079a2f725", + "Created": "2026-07-12T19:37:02.629972224-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.32.0.0/16", + "Gateway": "10.32.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "102d8ad3d8c342cc5525eb99cc0731ee0b583a571fa80bd84b35f4c0ced0de95", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiopixels", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "349fc11455f5e3fa641d086f12e6d9e671398d0b8c85058383ccf12f65ee61f0": { + "Name": "pixelfed-ma-redis", + "EndpointID": "229e6c03eb191c3e6a74ec46f04cb8327704f82db907597d8fa99f91471b2ff4", + "MacAddress": "32:1a:a8:a0:f3:37", + "IPv4Address": "10.32.0.2/16", + "IPv6Address": "" + }, + "61f7dd7992d22dcc66d0c2fad5bdce55adf152cde20020917ef92203ca84be57": { + "Name": "pixelfed-ma-web", + "EndpointID": "bd822e861bf41da09129b28cb7aa363a3bfd22d5b4e8fb0e7c6f7f8b6b2a6509", + "MacAddress": "7a:f3:e0:ec:99:d8", + "IPv4Address": "10.32.0.3/16", + "IPv6Address": "" + }, + "da9f7ee76a51464f64c25b359e5a3a776c8e960b08b5d477462a913c01f8d2d5": { + "Name": "pixelfed-ma-worker", + "EndpointID": "1d04ece195210b499b81cc7ae8337b47b2dda1a09c378d0879a06fd708b38989", + "MacAddress": "1a:80:80:9e:4b:65", + "IPv4Address": "10.32.0.4/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.32.0.0/16": { + "IPsInUse": 6, + "DynamicIPsAvailable": 65530 + } + } + } + } + }, + { + "Name": "privatebin_rw_net", + "Id": "f9343d8383acc2b0ed4409e4c3deeb198b046c2d6ccab05e3c6a122798f504b8", + "Created": "2026-07-12T19:37:02.150493495-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.24.0.0/16", + "Gateway": "10.24.0.1" + }, + { + "Subnet": "fd00:24::/64", + "Gateway": "fd00:24::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "7ba4add9c4c91e276bb08a1ef2e2305956e32e155027e7009876d33b93694c4b", + "com.docker.compose.network": "default", + "com.docker.compose.project": "makearmyiopaste", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "3755c654aa4859dbaef325bdd0f42300eccb4c6b1885186f55315e88733a8cee": { + "Name": "privatebin", + "EndpointID": "14fc7f307a4362fed8b588f73d8c79e6c30fab7e95ee78206bbe2811f7fd9e71", + "MacAddress": "26:de:af:e0:54:8f", + "IPv4Address": "10.24.0.2/16", + "IPv6Address": "fd00:24::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.24.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:24::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "s6vinetgames_default", + "Id": "71bcae37092b0d57aac754b07ce8c745b18a57b3efd55c344d0d93fc3036add6", + "Created": "2026-07-12T19:37:06.355054226-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.30.0.0/16", + "Gateway": "172.30.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "ccac200e5a0179722d7d50596ab8933e9a422bc19b3b574e42dc950fbc1afe8c", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetgames", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "0956a7ef4fd70d81d2e69e7ab1c336f0e895775375bffde4223dd9a84cfaa6ac": { + "Name": "romm", + "EndpointID": "509b738c413f9aa5c41029cc551fca5021d5cd915e422247d309e5f456b237e3", + "MacAddress": "d2:0f:10:92:12:e7", + "IPv4Address": "172.30.0.3/16", + "IPv6Address": "" + }, + "314a2835a830a0b37342467819193f05b7dfd6aed1e2ab9f46a12bd3356e1f51": { + "Name": "romm-db", + "EndpointID": "252c28a034774e1f47fcbe321fe8c004bd55d4dd859bc9ba3e44e4a22ebeeea7", + "MacAddress": "f6:86:fb:44:c1:c6", + "IPv4Address": "172.30.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.30.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + }, + { + "Name": "s6vinetmedia_media_s6_net", + "Id": "c50ccbc5e4647480621ab038843a64f90102d63ac104ad11a10e523f13f18d66", + "Created": "2026-07-12T19:37:17.269773208-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.19.0.0/16", + "Gateway": "10.19.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "cf64edb9c0f86344ea49b6ec7f88981801173f1fe5eb8418c66ec8ba25c180e3", + "com.docker.compose.network": "media_s6_net", + "com.docker.compose.project": "s6vinetmedia", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "9308279e99d674145978b82124062846fb20541d94a724481f858fc89f4d7fbb": { + "Name": "vpn", + "EndpointID": "42ec63152ff25cba49012918a9a32013be05495a29f4e7777e31f0dd43b0df81", + "MacAddress": "4e:4a:4c:49:02:51", + "IPv4Address": "10.19.0.2/16", + "IPv6Address": "" + }, + "b03e3702b0def70ce39a26a64655375027051dbeb448fc74b19c084136b1453d": { + "Name": "vpn-slskd", + "EndpointID": "d2b4745bc0b90465c281dab4d850f0f43ad6ea0faaa3170af9e83d003b313342", + "MacAddress": "6e:e5:1b:f8:32:3a", + "IPv4Address": "10.19.0.3/16", + "IPv6Address": "" + }, + "d20e85d4e22b40b62aa03f1a72750de0430649bc587b0d886d8a46de3b760798": { + "Name": "slskd-portwatch", + "EndpointID": "854f8e4db39da01962d7ea44476f7156f4328b29cdef9c006268a293ac30bd26", + "MacAddress": "26:27:e4:f6:7f:ac", + "IPv4Address": "10.19.0.4/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.19.0.0/16": { + "IPsInUse": 6, + "DynamicIPsAvailable": 65530 + } + } + } + } + }, + { + "Name": "s6vinetyt_default", + "Id": "9891a93b5e9c6a8f94eb8f56372ee50dcdd1719dd12899512a35372cbf2f29e8", + "Created": "2026-07-12T19:37:31.020812859-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "172.31.0.0/16", + "Gateway": "172.31.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "ad3308a627baf7d015b674d2c056bab6b1e01f8a41570e26f35a67273b9a6f65", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetyt", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "e317eae3c64b21537350c473a24165edc1f36e3a3ef1ffb4ad4393b94d45210e": { + "Name": "pinchflat", + "EndpointID": "7a7820393b006c341ac24656b14b9b82966040c99d1439621cc37dcc1d8554e8", + "MacAddress": "62:35:3a:7e:bf:79", + "IPv4Address": "172.31.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.31.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + } + } + } + } + }, + { + "Name": "snappy_net", + "Id": "9aea00f5a4853eb6be957deac2077c5b2186b67fedd09fffbb6d3ba784da1537", + "Created": "2026-07-12T19:36:40.273332364-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.3.0.0/16", + "Gateway": "10.3.0.1" + }, + { + "Subnet": "fd00:3::/64", + "Gateway": "fd00:3::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "2a9fa6ead8026ddf06a2d77cd80ad6a942c7f816c66d6787c23123bc9b00dca3", + "com.docker.compose.network": "default", + "com.docker.compose.project": "arrmailnet", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "ba0c4441758dcdbc273b068e9113a469c78f35b409555b1e9d2cf5a138ea3744": { + "Name": "snappymail", + "EndpointID": "7f39f6b798a4b858494fc62cc4082e1f87a3f957185f12785e9208db039fbf2b", + "MacAddress": "aa:1a:8c:4d:9c:82", + "IPv4Address": "10.3.0.2/16", + "IPv6Address": "fd00:3::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.3.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:3::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "vault_net", + "Id": "f63e542ab28cc3324fef8eca85b9746a39fc3bb09cf8108a4b0e28ec3dc11d28", + "Created": "2026-07-12T19:36:41.220745503-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": true, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.4.0.0/16", + "Gateway": "10.4.0.1" + }, + { + "Subnet": "fd00:4::/64", + "Gateway": "fd00:4::1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "1b6900b7bc4d509e011df4c37c33ff53a2946db523db3d7ea1858b2fd44791e5", + "com.docker.compose.network": "default", + "com.docker.compose.project": "arrmailnetpass", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "343d7cef8016d2650e85114664c71b1d4d0365337291d7fb9e1d47b7522620c9": { + "Name": "vaultwarden", + "EndpointID": "7635ea7a29a05d567f512c464a99ab7dd746121e3c3ae9e27db097636a670fe0", + "MacAddress": "e6:53:a9:07:8e:79", + "IPv4Address": "10.4.0.2/16", + "IPv6Address": "fd00:4::2/64" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.4.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + }, + "fd00:4::/64": { + "IPsInUse": 3, + "DynamicIPsAvailable": 18446744073709551613 + } + } + } + } + }, + { + "Name": "yacht_s6_net", + "Id": "a35cbfd37253d17df8587634ff74042d69261ed0bdc6236f1a9cdd111946d112", + "Created": "2026-07-12T19:37:06.031186656-04:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": null, + "Config": [ + { + "Subnet": "10.13.0.0/16", + "Gateway": "10.13.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": { + "com.docker.compose.config-hash": "3ce7b7de147a7da64695602e74159312b642e11363372e793e34ec18bef23336", + "com.docker.compose.network": "default", + "com.docker.compose.project": "s6vinetdocker", + "com.docker.compose.version": "5.3.1" + }, + "Containers": { + "ad90d0cb28fcfc58d588070894c7922a299b1d1598b99417393a154d5db09a63": { + "Name": "yacht", + "EndpointID": "d19afeab43bfee155e25ba12c78431f21630afe0fe0e9407cdea54c54c5af9be", + "MacAddress": "d2:e4:55:a5:58:87", + "IPv4Address": "10.13.0.2/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "10.13.0.0/16": { + "IPsInUse": 4, + "DynamicIPsAvailable": 65532 + } + } + } + } + } +] diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.psv b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.psv new file mode 100644 index 0000000..7622d32 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/docker-networks.psv @@ -0,0 +1,41 @@ +5a9988b94efd|arrmail_backend|bridge|local +cfae917b997e|arrmailnetmail_clamav|bridge|local +d89dcf384c2a|arrmailnetmail_oletools|bridge|local +b7976abb6e55|arrmailnetmail_radicale|bridge|local +ecba5adea6ea|arrmailnetmail_tika|bridge|local +e02b7c89a2d3|backend_net|bridge|local +a13cb8d34076|bridge|bridge|local +051bbcfc21d8|castopod_ma_net|bridge|local +28584d053169|collabora_s6_net|bridge|local +263c8508de6f|conduit_net|bridge|local +8b881e367322|directus_net|bridge|local +87e08370adff|forge_net|bridge|local +bca9ecea74f1|heimdall_s6_net|bridge|local +d905d3d625d9|host|host|local +4ecbee63f4b0|immich_s6_net|bridge|local +527a7cd84ff6|irc_net|bridge|local +8200b83f03e9|jellyfin_s6_net|bridge|local +6caddd639b0b|lasereverythingnetmanager_default|bridge|local +875fced92cd0|lasereverythingnettv_default|bridge|local +03ee8c9e4bea|lemmy_net|bridge|local +68e2ed0ec6ac|mailu_net|bridge|local +11ceaf8736f5|makearmy_net|bridge|local +2d50ca7a8f5d|makearmyiocast_default|bridge|local +eeee48934935|makearmyiodcm_default|bridge|local +dee363d4e0c6|makearmyiovdo_default|bridge|local +4efe811b8402|makearmyiowatch_peertube_net|bridge|local +f3f5c7cfa74e|mastodon_net|bridge|local +fea0382ed10b|meet_net|bridge|local +f10b040acc46|misskey_net|bridge|local +618855db6757|navidrome_s6_net|bridge|local +dc41f9983a47|nextcloud_ma_net|bridge|local +57b7955ca01d|none|null|local +ed1bb1cc9d39|picsur_net|bridge|local +59ca07fbf200|pixelfed_ma_net|bridge|local +f9343d8383ac|privatebin_rw_net|bridge|local +71bcae37092b|s6vinetgames_default|bridge|local +c50ccbc5e464|s6vinetmedia_media_s6_net|bridge|local +9891a93b5e9c|s6vinetyt_default|bridge|local +9aea00f5a485|snappy_net|bridge|local +f63e542ab28c|vault_net|bridge|local +a35cbfd37253|yacht_s6_net|bridge|local diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/host-network.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/host-network.txt new file mode 100644 index 0000000..b334e9c --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/host-network.txt @@ -0,0 +1,885 @@ +=== IPv4/IPv6 addresses === +lo UNKNOWN 127.0.0.1/8 ::1/128 +enp4s0 UP 192.168.10.151/24 2603:7080:7500:1279:75aa:d64d:4cf0:8f10/64 fe80::e6f:317c:ee82:e5e/64 +wlo1 DOWN +wg0 UNKNOWN 10.98.0.2/24 +br-11ceaf8736f5 UP 10.55.0.1/16 fe80::a065:eaff:fef3:532d/64 +br-dee363d4e0c6 DOWN 172.29.0.1/16 fe80::b86e:58ff:fef5:85f6/64 +docker0 UP 172.17.0.1/16 fe80::48e9:9ff:fea5:2c72/64 +br-5a9988b94efd UP 10.1.0.1/16 fd00:1::1/64 fe80::a843:f3ff:fedb:ef64/64 +br-e02b7c89a2d3 UP 10.99.0.1/16 fe80::2c30:58ff:fe89:f9d2/64 +br-875fced92cd0 DOWN 172.25.0.1/16 fe80::6c7e:7fff:fe2a:bf1d/64 +vethf79d976@if2 UP fe80::4c52:c7ff:fe49:3b0e/64 +veth605aea1@if2 UP fe80::ec3f:78ff:fe38:75b9/64 +vethf583175@if2 UP fe80::f0c3:fff:fe8b:74e3/64 +vethca028f8@if2 UP fe80::ccd1:f1ff:fe0f:c699/64 +veth880eb86@if2 UP fe80::548d:f8ff:fef4:c0db/64 +veth7e2863d@if2 UP fe80::ac5e:9cff:fe45:ec06/64 +veth55cb2ec@if2 UP fe80::28b8:bdff:fe81:370f/64 +br-9aea00f5a485 UP 10.3.0.1/16 fd00:3::1/64 fe80::68b1:bff:fee2:9d17/64 +vethee5b2b0@if2 UP fe80::6486:bfff:fee7:22df/64 +br-68e2ed0ec6ac UP 10.2.0.1/16 fe80::1058:f8ff:fe69:21bb/64 +br-b7976abb6e55 UP 172.18.0.1/16 fe80::a8fe:4ff:fe1f:d81a/64 +br-ecba5adea6ea UP 172.19.0.1/16 fe80::f8d4:eff:fe6e:19a9/64 +br-cfae917b997e UP 172.20.0.1/16 fe80::60fc:94ff:fe7d:a58f/64 +br-d89dcf384c2a UP 172.21.0.1/16 fe80::b471:19ff:fe52:ff1/64 +vethbe8f01c@if2 UP fe80::63:73ff:fede:43f9/64 +veth38ae454@if2 UP fe80::f01a:4eff:fef2:9942/64 +veth2edd7d6@if2 UP fe80::3c0c:90ff:fe10:332f/64 +veth563d3b9@if2 UP fe80::d037:e3ff:fe79:1a5f/64 +veth766794b@if2 UP fe80::3441:76ff:fe3f:f8d1/64 +veth426f4c7@if2 UP fe80::74c2:12ff:fe72:7875/64 +veth362d4c6@if2 UP fe80::f038:dbff:fefd:87d3/64 +veth4ecfbb5@if2 UP fe80::d847:aaff:fe89:3f9/64 +veth2d01312@if3 UP fe80::5455:fbff:fec6:391e/64 +veth99bb318@if2 UP fe80::4a3:bbff:fea9:df98/64 +vethcae9493@if2 UP fe80::2844:4dff:fec7:1613/64 +veth8c83268@if2 UP fe80::8832:52ff:fee4:8a93/64 +veth6f6f074@if3 UP fe80::ac67:76ff:fe15:1676/64 +veth4216565@if3 UP fe80::4463:6dff:fe29:79d5/64 +veth46248ff@if4 UP fe80::18c7:26ff:fe8b:bea4/64 +vethc0ef06c@if2 UP fe80::5852:d0ff:fe9e:545/64 +br-f63e542ab28c UP 10.4.0.1/16 fd00:4::1/64 fe80::28df:aff:fe50:a6d2/64 +vethaeee758@if2 UP fe80::e807:c7ff:fee0:f12d/64 +br-8b881e367322 DOWN 10.40.0.1/16 fd00:40::1/64 fe80::c030:54ff:fe2d:eb36/64 +br-6caddd639b0b UP 172.22.0.1/16 fe80::c0da:5bff:fea4:92dc/64 +veth2906c94@if2 UP fe80::1c13:3ff:fe18:c9a4/64 +br-2d50ca7a8f5d UP 172.23.0.1/16 fe80::548d:1eff:fe36:12c5/64 +vethbb40f36@if2 UP fe80::c000:96ff:fe7d:11a8/64 +br-dc41f9983a47 UP 10.30.0.1/16 fd00:30::1/64 fe80::78cf:3dff:fe72:528b/64 +veth6594ba0@if2 UP fe80::9091:4aff:feee:8942/64 +veth28958e3@if2 UP fe80::d037:82ff:fee7:27c5/64 +veth1f2dc82@if3 UP fe80::e49e:23ff:feb7:932d/64 +veth04a7fbe@if4 UP fe80::e488:ccff:fe2f:e545/64 +veth2273529@if2 UP fe80::869:a1ff:fee9:63a6/64 +br-eeee48934935 UP 172.24.0.1/16 fe80::ac86:86ff:fe64:f2d4/64 +veth160d05f@if2 UP fe80::50f3:d6ff:fedd:1cd3/64 +br-f10b040acc46 UP 10.29.0.1/16 fd00:29::1/64 fe80::4c63:fcff:fe4c:7d6a/64 +vethd88eb9b@if2 UP fe80::a4a0:13ff:fe7f:7c8d/64 +veth80da983@if2 UP fe80::a0eb:3bff:fe66:df68/64 +veth2bde217@if3 UP fe80::24de:7fff:fef9:e7af/64 +br-87e08370adff UP 10.27.0.1/16 fd00:27::1/64 fe80::40f5:bfff:fed7:8296/64 +veth9e2bcbb@if2 UP fe80::5030:beff:fead:c50a/64 +veth0477205@if3 UP fe80::48ec:7cff:fe17:27bc/64 +br-ed1bb1cc9d39 UP 10.28.0.1/16 fd00:28::1/64 fe80::30d4:6bff:fe54:5fb3/64 +veth370f490@if2 UP fe80::c090:5eff:fed8:d1cc/64 +vethca0ee65@if2 UP fe80::c1f:b5ff:fe0a:c936/64 +veth4692f5c@if3 UP fe80::2c60:58ff:fe86:30d3/64 +br-527a7cd84ff6 UP 10.20.0.1/16 fd00:20::1/64 fe80::4482:41ff:feb3:9c74/64 +vethd14eb7e@if2 UP fe80::84bf:b2ff:fec4:ea7/64 +br-03ee8c9e4bea UP 172.28.0.1/16 fe80::f01f:e5ff:fe2b:ec80/64 +veth3c1861e@if2 UP fe80::1c6e:c5ff:fe94:c515/64 +veth32d5964@if2 UP fe80::80a3:1eff:fe61:4f10/64 +vetha548a1a@if3 UP fe80::3c6e:51ff:fe48:e037/64 +veth762d601@if2 UP fe80::424:9bff:fe91:317e/64 +vethf48aa55@if2 UP fe80::805a:47ff:fe53:cfcd/64 +br-f3f5c7cfa74e UP 10.31.0.1/16 fd00:31::1/64 fe80::f4a6:b6ff:fe24:af71/64 +vethf5f3d34@if2 UP fe80::c006:5ff:fea6:b743/64 +veth335556c@if2 UP fe80::90ed:30ff:fe51:2b5a/64 +veth9cc2ad8@if2 UP fe80::6c50:96ff:fe29:70a6/64 +veth6232cb7@if2 UP fe80::64fe:2aff:fed1:1e41/64 +veth337e2fd@if2 UP fe80::f0bf:5ff:fee7:2b1e/64 +veth7d6d658@if3 UP fe80::80fb:8fff:fead:2208/64 +veth448a204@if3 UP fe80::90ff:b0ff:fe58:21fb/64 +veth79ea708@if3 UP fe80::ac43:75ff:fefa:cbfd/64 +br-263c8508de6f UP 10.26.0.1/16 fd00:26::1/64 fe80::d8c8:aaff:fe63:6af9/64 +veth4ba8ed1@if2 UP fe80::2095:66ff:fe78:518e/64 +br-fea0382ed10b UP 172.26.0.1/16 fe80::6c9c:c1ff:fe95:888e/64 +veth4a87e58@if2 UP fe80::c899:1fff:fe27:7f66/64 +vethacfaa19@if2 UP fe80::90ed:f4ff:feed:a749/64 +veth30db54a@if2 UP fe80::e855:acff:fef2:5aa3/64 +vethe969d14@if2 UP fe80::303f:6ff:fe24:1fa6/64 +veth0e4600c@if2 UP fe80::b441:acff:fe66:5981/64 +br-f9343d8383ac UP 10.24.0.1/16 fd00:24::1/64 fe80::c8b0:56ff:fe7e:bc48/64 +veth21e2097@if2 UP fe80::b48b:1aff:fe71:36d3/64 +br-59ca07fbf200 UP 10.32.0.1/16 fe80::fc79:ccff:fefd:20d7/64 +veth58179de@if2 UP fe80::6cb5:cff:fea2:a675/64 +veth6b9d50d@if2 UP fe80::d45e:64ff:fe90:37e7/64 +veth2445f8e@if2 UP fe80::cc80:50ff:feb5:67b/64 +veth368657c@if3 UP fe80::a065:bfff:fea5:34/64 +veth5f6906b@if3 UP fe80::502a:e5ff:fe5c:d5fa/64 +veth2dd045f@if4 UP fe80::464:d7ff:fea1:a54a/64 +veth53fd806@if4 UP fe80::7cdd:44ff:fe90:1f77/64 +br-051bbcfc21d8 UP 10.41.0.1/16 fd00:41::1/64 fe80::d457:2bff:fe72:23f/64 +veth2b9371a@if2 UP fe80::d44c:3bff:fe3b:934f/64 +veth165b4aa@if2 UP fe80::c85e:5aff:feaa:f812/64 +veth8c376e6@if3 UP fe80::542a:61ff:fe38:307c/64 +veth73c775b@if4 UP fe80::7462:7dff:fe8f:115d/64 +br-4efe811b8402 UP 172.27.0.1/16 fe80::d87e:fbff:fec6:5aeb/64 +veth56166e3@if2 UP fe80::dc0f:5dff:fec4:7c53/64 +veth14193e1@if2 UP fe80::aca7:68ff:fe42:e77/64 +vethe9fcee3@if3 UP fe80::443:71ff:feac:bdfb/64 +br-bca9ecea74f1 UP 10.10.0.1/16 fd00:10::1/64 fe80::4421:95ff:fe94:d7c5/64 +veth71028db@if2 UP fe80::a4ab:b9ff:fe3e:af94/64 +br-a35cbfd37253 UP 10.13.0.1/16 fe80::aca7:12ff:fe18:d1b9/64 +vethe123758@if2 UP fe80::44ee:8eff:fe1a:bcf4/64 +br-71bcae37092b UP 172.30.0.1/16 fe80::286c:a2ff:fe49:5772/64 +veth8d698e0@if2 UP fe80::d86b:4fff:fec2:50e0/64 +vethd87ba34@if2 UP fe80::e87f:edff:fe10:e8d6/64 +br-c50ccbc5e464 UP 10.19.0.1/16 fe80::9879:f6ff:fe6d:57be/64 +veth1bece5c@if2 UP fe80::30f1:2cff:feac:90cb/64 +veth6091231@if2 UP fe80::d44d:feff:fe18:d291/64 +veth502e53b@if2 UP fe80::a047:6eff:fe8b:1f48/64 +br-618855db6757 UP 10.16.0.1/16 fd00:16::1/64 fe80::aca8:14ff:feee:df7c/64 +vethe6c6432@if2 UP fe80::e447:bff:feea:1f61/64 +veth1f1f066@if3 UP fe80::90da:e3ff:fefa:7464/64 +veth26a5545@if4 UP fe80::888d:45ff:feeb:4a48/64 +br-28584d053169 UP 10.17.0.1/16 fd00:17::1/64 fe80::7c73:c8ff:fe04:3dd2/64 +vethe488043@if2 UP fe80::7069:c0ff:febd:1d88/64 +br-4ecbee63f4b0 UP 10.18.0.1/16 fd00:18::1/64 fe80::e46c:23ff:fec3:3ab9/64 +veth15eb212@if2 UP fe80::5c80:75ff:feff:22dd/64 +veth551b749@if2 UP fe80::7c42:75ff:fe80:5953/64 +veth41b1943@if2 UP fe80::bc01:62ff:fe13:b616/64 +vethc2b3531@if3 UP fe80::b4d5:57ff:fe87:83f9/64 +br-8200b83f03e9 UP 10.9.0.1/16 fd00:9::1/64 fe80::4c80:8fff:feec:15d4/64 +veth62aa43d@if2 UP fe80::acc5:57ff:fece:3b51/64 +br-9891a93b5e9c UP 172.31.0.1/16 fe80::e0a6:32ff:fe63:9f8d/64 +veth081c8bf@if2 UP fe80::3c1f:f9ff:fe36:4122/64 + +=== IPv4 routes === +default via 10.98.0.1 dev wg0 table wg0 +default via 192.168.10.1 dev enp4s0 proto dhcp src 192.168.10.151 metric 100 +10.1.0.0/16 dev br-5a9988b94efd proto kernel scope link src 10.1.0.1 +10.2.0.0/16 dev br-68e2ed0ec6ac proto kernel scope link src 10.2.0.1 +10.3.0.0/16 dev br-9aea00f5a485 proto kernel scope link src 10.3.0.1 +10.4.0.0/16 dev br-f63e542ab28c proto kernel scope link src 10.4.0.1 +10.9.0.0/16 dev br-8200b83f03e9 proto kernel scope link src 10.9.0.1 +10.10.0.0/16 dev br-bca9ecea74f1 proto kernel scope link src 10.10.0.1 +10.13.0.0/16 dev br-a35cbfd37253 proto kernel scope link src 10.13.0.1 +10.16.0.0/16 dev br-618855db6757 proto kernel scope link src 10.16.0.1 +10.17.0.0/16 dev br-28584d053169 proto kernel scope link src 10.17.0.1 +10.18.0.0/16 dev br-4ecbee63f4b0 proto kernel scope link src 10.18.0.1 +10.19.0.0/16 dev br-c50ccbc5e464 proto kernel scope link src 10.19.0.1 +10.20.0.0/16 dev br-527a7cd84ff6 proto kernel scope link src 10.20.0.1 +10.24.0.0/16 dev br-f9343d8383ac proto kernel scope link src 10.24.0.1 +10.26.0.0/16 dev br-263c8508de6f proto kernel scope link src 10.26.0.1 +10.27.0.0/16 dev br-87e08370adff proto kernel scope link src 10.27.0.1 +10.28.0.0/16 dev br-ed1bb1cc9d39 proto kernel scope link src 10.28.0.1 +10.29.0.0/16 dev br-f10b040acc46 proto kernel scope link src 10.29.0.1 +10.30.0.0/16 dev br-dc41f9983a47 proto kernel scope link src 10.30.0.1 +10.31.0.0/16 dev br-f3f5c7cfa74e proto kernel scope link src 10.31.0.1 +10.32.0.0/16 dev br-59ca07fbf200 proto kernel scope link src 10.32.0.1 +10.40.0.0/16 dev br-8b881e367322 proto kernel scope link src 10.40.0.1 linkdown +10.41.0.0/16 dev br-051bbcfc21d8 proto kernel scope link src 10.41.0.1 +10.55.0.0/16 dev br-11ceaf8736f5 proto kernel scope link src 10.55.0.1 +10.98.0.0/24 dev wg0 proto kernel scope link src 10.98.0.2 +10.99.0.0/16 dev br-e02b7c89a2d3 proto kernel scope link src 10.99.0.1 +172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 +172.18.0.0/16 dev br-b7976abb6e55 proto kernel scope link src 172.18.0.1 +172.19.0.0/16 dev br-ecba5adea6ea proto kernel scope link src 172.19.0.1 +172.20.0.0/16 dev br-cfae917b997e proto kernel scope link src 172.20.0.1 +172.21.0.0/16 dev br-d89dcf384c2a proto kernel scope link src 172.21.0.1 +172.22.0.0/16 dev br-6caddd639b0b proto kernel scope link src 172.22.0.1 +172.23.0.0/16 dev br-2d50ca7a8f5d proto kernel scope link src 172.23.0.1 +172.24.0.0/16 dev br-eeee48934935 proto kernel scope link src 172.24.0.1 +172.25.0.0/16 dev br-875fced92cd0 proto kernel scope link src 172.25.0.1 linkdown +172.26.0.0/16 dev br-fea0382ed10b proto kernel scope link src 172.26.0.1 +172.27.0.0/16 dev br-4efe811b8402 proto kernel scope link src 172.27.0.1 +172.28.0.0/16 dev br-03ee8c9e4bea proto kernel scope link src 172.28.0.1 +172.29.0.0/16 dev br-dee363d4e0c6 proto kernel scope link src 172.29.0.1 linkdown +172.30.0.0/16 dev br-71bcae37092b proto kernel scope link src 172.30.0.1 +172.31.0.0/16 dev br-9891a93b5e9c proto kernel scope link src 172.31.0.1 +192.168.10.0/24 dev enp4s0 proto kernel scope link src 192.168.10.151 metric 100 +local 10.1.0.1 dev br-5a9988b94efd table local proto kernel scope host src 10.1.0.1 +broadcast 10.1.255.255 dev br-5a9988b94efd table local proto kernel scope link src 10.1.0.1 +local 10.2.0.1 dev br-68e2ed0ec6ac table local proto kernel scope host src 10.2.0.1 +broadcast 10.2.255.255 dev br-68e2ed0ec6ac table local proto kernel scope link src 10.2.0.1 +local 10.3.0.1 dev br-9aea00f5a485 table local proto kernel scope host src 10.3.0.1 +broadcast 10.3.255.255 dev br-9aea00f5a485 table local proto kernel scope link src 10.3.0.1 +local 10.4.0.1 dev br-f63e542ab28c table local proto kernel scope host src 10.4.0.1 +broadcast 10.4.255.255 dev br-f63e542ab28c table local proto kernel scope link src 10.4.0.1 +local 10.9.0.1 dev br-8200b83f03e9 table local proto kernel scope host src 10.9.0.1 +broadcast 10.9.255.255 dev br-8200b83f03e9 table local proto kernel scope link src 10.9.0.1 +local 10.10.0.1 dev br-bca9ecea74f1 table local proto kernel scope host src 10.10.0.1 +broadcast 10.10.255.255 dev br-bca9ecea74f1 table local proto kernel scope link src 10.10.0.1 +local 10.13.0.1 dev br-a35cbfd37253 table local proto kernel scope host src 10.13.0.1 +broadcast 10.13.255.255 dev br-a35cbfd37253 table local proto kernel scope link src 10.13.0.1 +local 10.16.0.1 dev br-618855db6757 table local proto kernel scope host src 10.16.0.1 +broadcast 10.16.255.255 dev br-618855db6757 table local proto kernel scope link src 10.16.0.1 +local 10.17.0.1 dev br-28584d053169 table local proto kernel scope host src 10.17.0.1 +broadcast 10.17.255.255 dev br-28584d053169 table local proto kernel scope link src 10.17.0.1 +local 10.18.0.1 dev br-4ecbee63f4b0 table local proto kernel scope host src 10.18.0.1 +broadcast 10.18.255.255 dev br-4ecbee63f4b0 table local proto kernel scope link src 10.18.0.1 +local 10.19.0.1 dev br-c50ccbc5e464 table local proto kernel scope host src 10.19.0.1 +broadcast 10.19.255.255 dev br-c50ccbc5e464 table local proto kernel scope link src 10.19.0.1 +local 10.20.0.1 dev br-527a7cd84ff6 table local proto kernel scope host src 10.20.0.1 +broadcast 10.20.255.255 dev br-527a7cd84ff6 table local proto kernel scope link src 10.20.0.1 +local 10.24.0.1 dev br-f9343d8383ac table local proto kernel scope host src 10.24.0.1 +broadcast 10.24.255.255 dev br-f9343d8383ac table local proto kernel scope link src 10.24.0.1 +local 10.26.0.1 dev br-263c8508de6f table local proto kernel scope host src 10.26.0.1 +broadcast 10.26.255.255 dev br-263c8508de6f table local proto kernel scope link src 10.26.0.1 +local 10.27.0.1 dev br-87e08370adff table local proto kernel scope host src 10.27.0.1 +broadcast 10.27.255.255 dev br-87e08370adff table local proto kernel scope link src 10.27.0.1 +local 10.28.0.1 dev br-ed1bb1cc9d39 table local proto kernel scope host src 10.28.0.1 +broadcast 10.28.255.255 dev br-ed1bb1cc9d39 table local proto kernel scope link src 10.28.0.1 +local 10.29.0.1 dev br-f10b040acc46 table local proto kernel scope host src 10.29.0.1 +broadcast 10.29.255.255 dev br-f10b040acc46 table local proto kernel scope link src 10.29.0.1 +local 10.30.0.1 dev br-dc41f9983a47 table local proto kernel scope host src 10.30.0.1 +broadcast 10.30.255.255 dev br-dc41f9983a47 table local proto kernel scope link src 10.30.0.1 +local 10.31.0.1 dev br-f3f5c7cfa74e table local proto kernel scope host src 10.31.0.1 +broadcast 10.31.255.255 dev br-f3f5c7cfa74e table local proto kernel scope link src 10.31.0.1 +local 10.32.0.1 dev br-59ca07fbf200 table local proto kernel scope host src 10.32.0.1 +broadcast 10.32.255.255 dev br-59ca07fbf200 table local proto kernel scope link src 10.32.0.1 +local 10.40.0.1 dev br-8b881e367322 table local proto kernel scope host src 10.40.0.1 +broadcast 10.40.255.255 dev br-8b881e367322 table local proto kernel scope link src 10.40.0.1 linkdown +local 10.41.0.1 dev br-051bbcfc21d8 table local proto kernel scope host src 10.41.0.1 +broadcast 10.41.255.255 dev br-051bbcfc21d8 table local proto kernel scope link src 10.41.0.1 +local 10.55.0.1 dev br-11ceaf8736f5 table local proto kernel scope host src 10.55.0.1 +broadcast 10.55.255.255 dev br-11ceaf8736f5 table local proto kernel scope link src 10.55.0.1 +local 10.98.0.2 dev wg0 table local proto kernel scope host src 10.98.0.2 +broadcast 10.98.0.255 dev wg0 table local proto kernel scope link src 10.98.0.2 +local 10.99.0.1 dev br-e02b7c89a2d3 table local proto kernel scope host src 10.99.0.1 +broadcast 10.99.255.255 dev br-e02b7c89a2d3 table local proto kernel scope link src 10.99.0.1 +local 127.0.0.0/8 dev lo table local proto kernel scope host src 127.0.0.1 +local 127.0.0.1 dev lo table local proto kernel scope host src 127.0.0.1 +broadcast 127.255.255.255 dev lo table local proto kernel scope link src 127.0.0.1 +local 172.17.0.1 dev docker0 table local proto kernel scope host src 172.17.0.1 +broadcast 172.17.255.255 dev docker0 table local proto kernel scope link src 172.17.0.1 +local 172.18.0.1 dev br-b7976abb6e55 table local proto kernel scope host src 172.18.0.1 +broadcast 172.18.255.255 dev br-b7976abb6e55 table local proto kernel scope link src 172.18.0.1 +local 172.19.0.1 dev br-ecba5adea6ea table local proto kernel scope host src 172.19.0.1 +broadcast 172.19.255.255 dev br-ecba5adea6ea table local proto kernel scope link src 172.19.0.1 +local 172.20.0.1 dev br-cfae917b997e table local proto kernel scope host src 172.20.0.1 +broadcast 172.20.255.255 dev br-cfae917b997e table local proto kernel scope link src 172.20.0.1 +local 172.21.0.1 dev br-d89dcf384c2a table local proto kernel scope host src 172.21.0.1 +broadcast 172.21.255.255 dev br-d89dcf384c2a table local proto kernel scope link src 172.21.0.1 +local 172.22.0.1 dev br-6caddd639b0b table local proto kernel scope host src 172.22.0.1 +broadcast 172.22.255.255 dev br-6caddd639b0b table local proto kernel scope link src 172.22.0.1 +local 172.23.0.1 dev br-2d50ca7a8f5d table local proto kernel scope host src 172.23.0.1 +broadcast 172.23.255.255 dev br-2d50ca7a8f5d table local proto kernel scope link src 172.23.0.1 +local 172.24.0.1 dev br-eeee48934935 table local proto kernel scope host src 172.24.0.1 +broadcast 172.24.255.255 dev br-eeee48934935 table local proto kernel scope link src 172.24.0.1 +local 172.25.0.1 dev br-875fced92cd0 table local proto kernel scope host src 172.25.0.1 +broadcast 172.25.255.255 dev br-875fced92cd0 table local proto kernel scope link src 172.25.0.1 linkdown +local 172.26.0.1 dev br-fea0382ed10b table local proto kernel scope host src 172.26.0.1 +broadcast 172.26.255.255 dev br-fea0382ed10b table local proto kernel scope link src 172.26.0.1 +local 172.27.0.1 dev br-4efe811b8402 table local proto kernel scope host src 172.27.0.1 +broadcast 172.27.255.255 dev br-4efe811b8402 table local proto kernel scope link src 172.27.0.1 +local 172.28.0.1 dev br-03ee8c9e4bea table local proto kernel scope host src 172.28.0.1 +broadcast 172.28.255.255 dev br-03ee8c9e4bea table local proto kernel scope link src 172.28.0.1 +local 172.29.0.1 dev br-dee363d4e0c6 table local proto kernel scope host src 172.29.0.1 +broadcast 172.29.255.255 dev br-dee363d4e0c6 table local proto kernel scope link src 172.29.0.1 linkdown +local 172.30.0.1 dev br-71bcae37092b table local proto kernel scope host src 172.30.0.1 +broadcast 172.30.255.255 dev br-71bcae37092b table local proto kernel scope link src 172.30.0.1 +local 172.31.0.1 dev br-9891a93b5e9c table local proto kernel scope host src 172.31.0.1 +broadcast 172.31.255.255 dev br-9891a93b5e9c table local proto kernel scope link src 172.31.0.1 +local 192.168.10.151 dev enp4s0 table local proto kernel scope host src 192.168.10.151 +broadcast 192.168.10.255 dev enp4s0 table local proto kernel scope link src 192.168.10.151 + +=== IPv6 routes === +2603:7080:7500:1279::/64 dev enp4s0 proto ra metric 100 pref medium +fd00:1::/64 dev br-5a9988b94efd proto kernel metric 256 pref medium +fd00:3::/64 dev br-9aea00f5a485 proto kernel metric 256 pref medium +fd00:4::/64 dev br-f63e542ab28c proto kernel metric 256 pref medium +fd00:9::/64 dev br-8200b83f03e9 proto kernel metric 256 pref medium +fd00:10::/64 dev br-bca9ecea74f1 proto kernel metric 256 pref medium +fd00:16::/64 dev br-618855db6757 proto kernel metric 256 pref medium +fd00:17::/64 dev br-28584d053169 proto kernel metric 256 pref medium +fd00:18::/64 dev br-4ecbee63f4b0 proto kernel metric 256 pref medium +fd00:20::/64 dev br-527a7cd84ff6 proto kernel metric 256 pref medium +fd00:24::/64 dev br-f9343d8383ac proto kernel metric 256 pref medium +fd00:26::/64 dev br-263c8508de6f proto kernel metric 256 pref medium +fd00:27::/64 dev br-87e08370adff proto kernel metric 256 pref medium +fd00:28::/64 dev br-ed1bb1cc9d39 proto kernel metric 256 pref medium +fd00:29::/64 dev br-f10b040acc46 proto kernel metric 256 pref medium +fd00:30::/64 dev br-dc41f9983a47 proto kernel metric 256 pref medium +fd00:31::/64 dev br-f3f5c7cfa74e proto kernel metric 256 pref medium +fd00:40::/64 dev br-8b881e367322 proto kernel metric 256 linkdown pref medium +fd00:41::/64 dev br-051bbcfc21d8 proto kernel metric 256 pref medium +fe80::/64 dev vethf79d976 proto kernel metric 256 pref medium +fe80::/64 dev docker0 proto kernel metric 256 pref medium +fe80::/64 dev veth605aea1 proto kernel metric 256 pref medium +fe80::/64 dev br-5a9988b94efd proto kernel metric 256 pref medium +fe80::/64 dev vethca028f8 proto kernel metric 256 pref medium +fe80::/64 dev br-e02b7c89a2d3 proto kernel metric 256 pref medium +fe80::/64 dev br-dee363d4e0c6 proto kernel metric 256 linkdown pref medium +fe80::/64 dev br-875fced92cd0 proto kernel metric 256 linkdown pref medium +fe80::/64 dev vethf583175 proto kernel metric 256 pref medium +fe80::/64 dev br-11ceaf8736f5 proto kernel metric 256 pref medium +fe80::/64 dev veth880eb86 proto kernel metric 256 pref medium +fe80::/64 dev veth7e2863d proto kernel metric 256 pref medium +fe80::/64 dev veth55cb2ec proto kernel metric 256 pref medium +fe80::/64 dev vethee5b2b0 proto kernel metric 256 pref medium +fe80::/64 dev br-9aea00f5a485 proto kernel metric 256 pref medium +fe80::/64 dev vethbe8f01c proto kernel metric 256 pref medium +fe80::/64 dev br-b7976abb6e55 proto kernel metric 256 pref medium +fe80::/64 dev veth2edd7d6 proto kernel metric 256 pref medium +fe80::/64 dev br-68e2ed0ec6ac proto kernel metric 256 pref medium +fe80::/64 dev veth38ae454 proto kernel metric 256 pref medium +fe80::/64 dev br-cfae917b997e proto kernel metric 256 pref medium +fe80::/64 dev veth563d3b9 proto kernel metric 256 pref medium +fe80::/64 dev br-ecba5adea6ea proto kernel metric 256 pref medium +fe80::/64 dev veth766794b proto kernel metric 256 pref medium +fe80::/64 dev br-d89dcf384c2a proto kernel metric 256 pref medium +fe80::/64 dev veth426f4c7 proto kernel metric 256 pref medium +fe80::/64 dev veth362d4c6 proto kernel metric 256 pref medium +fe80::/64 dev veth2d01312 proto kernel metric 256 pref medium +fe80::/64 dev veth4ecfbb5 proto kernel metric 256 pref medium +fe80::/64 dev veth99bb318 proto kernel metric 256 pref medium +fe80::/64 dev vethcae9493 proto kernel metric 256 pref medium +fe80::/64 dev veth8c83268 proto kernel metric 256 pref medium +fe80::/64 dev veth6f6f074 proto kernel metric 256 pref medium +fe80::/64 dev veth4216565 proto kernel metric 256 pref medium +fe80::/64 dev veth46248ff proto kernel metric 256 pref medium +fe80::/64 dev vethc0ef06c proto kernel metric 256 pref medium +fe80::/64 dev vethaeee758 proto kernel metric 256 pref medium +fe80::/64 dev br-f63e542ab28c proto kernel metric 256 pref medium +fe80::/64 dev br-8b881e367322 proto kernel metric 256 linkdown pref medium +fe80::/64 dev veth2906c94 proto kernel metric 256 pref medium +fe80::/64 dev br-6caddd639b0b proto kernel metric 256 pref medium +fe80::/64 dev vethbb40f36 proto kernel metric 256 pref medium +fe80::/64 dev br-2d50ca7a8f5d proto kernel metric 256 pref medium +fe80::/64 dev veth6594ba0 proto kernel metric 256 pref medium +fe80::/64 dev br-dc41f9983a47 proto kernel metric 256 pref medium +fe80::/64 dev veth28958e3 proto kernel metric 256 pref medium +fe80::/64 dev veth1f2dc82 proto kernel metric 256 pref medium +fe80::/64 dev veth04a7fbe proto kernel metric 256 pref medium +fe80::/64 dev veth2273529 proto kernel metric 256 pref medium +fe80::/64 dev veth160d05f proto kernel metric 256 pref medium +fe80::/64 dev br-eeee48934935 proto kernel metric 256 pref medium +fe80::/64 dev vethd88eb9b proto kernel metric 256 pref medium +fe80::/64 dev br-f10b040acc46 proto kernel metric 256 pref medium +fe80::/64 dev veth80da983 proto kernel metric 256 pref medium +fe80::/64 dev veth2bde217 proto kernel metric 256 pref medium +fe80::/64 dev veth9e2bcbb proto kernel metric 256 pref medium +fe80::/64 dev veth0477205 proto kernel metric 256 pref medium +fe80::/64 dev br-87e08370adff proto kernel metric 256 pref medium +fe80::/64 dev veth370f490 proto kernel metric 256 pref medium +fe80::/64 dev br-ed1bb1cc9d39 proto kernel metric 256 pref medium +fe80::/64 dev vethca0ee65 proto kernel metric 256 pref medium +fe80::/64 dev veth4692f5c proto kernel metric 256 pref medium +fe80::/64 dev vethd14eb7e proto kernel metric 256 pref medium +fe80::/64 dev br-527a7cd84ff6 proto kernel metric 256 pref medium +fe80::/64 dev veth3c1861e proto kernel metric 256 pref medium +fe80::/64 dev br-03ee8c9e4bea proto kernel metric 256 pref medium +fe80::/64 dev veth32d5964 proto kernel metric 256 pref medium +fe80::/64 dev vetha548a1a proto kernel metric 256 pref medium +fe80::/64 dev veth762d601 proto kernel metric 256 pref medium +fe80::/64 dev vethf48aa55 proto kernel metric 256 pref medium +fe80::/64 dev vethf5f3d34 proto kernel metric 256 pref medium +fe80::/64 dev br-f3f5c7cfa74e proto kernel metric 256 pref medium +fe80::/64 dev veth335556c proto kernel metric 256 pref medium +fe80::/64 dev veth9cc2ad8 proto kernel metric 256 pref medium +fe80::/64 dev veth6232cb7 proto kernel metric 256 pref medium +fe80::/64 dev veth337e2fd proto kernel metric 256 pref medium +fe80::/64 dev veth7d6d658 proto kernel metric 256 pref medium +fe80::/64 dev veth448a204 proto kernel metric 256 pref medium +fe80::/64 dev veth79ea708 proto kernel metric 256 pref medium +fe80::/64 dev veth4ba8ed1 proto kernel metric 256 pref medium +fe80::/64 dev br-263c8508de6f proto kernel metric 256 pref medium +fe80::/64 dev veth4a87e58 proto kernel metric 256 pref medium +fe80::/64 dev br-fea0382ed10b proto kernel metric 256 pref medium +fe80::/64 dev vethacfaa19 proto kernel metric 256 pref medium +fe80::/64 dev veth30db54a proto kernel metric 256 pref medium +fe80::/64 dev vethe969d14 proto kernel metric 256 pref medium +fe80::/64 dev veth0e4600c proto kernel metric 256 pref medium +fe80::/64 dev veth21e2097 proto kernel metric 256 pref medium +fe80::/64 dev br-f9343d8383ac proto kernel metric 256 pref medium +fe80::/64 dev veth58179de proto kernel metric 256 pref medium +fe80::/64 dev br-59ca07fbf200 proto kernel metric 256 pref medium +fe80::/64 dev veth6b9d50d proto kernel metric 256 pref medium +fe80::/64 dev veth2445f8e proto kernel metric 256 pref medium +fe80::/64 dev veth368657c proto kernel metric 256 pref medium +fe80::/64 dev veth5f6906b proto kernel metric 256 pref medium +fe80::/64 dev veth2dd045f proto kernel metric 256 pref medium +fe80::/64 dev veth53fd806 proto kernel metric 256 pref medium +fe80::/64 dev veth2b9371a proto kernel metric 256 pref medium +fe80::/64 dev br-051bbcfc21d8 proto kernel metric 256 pref medium +fe80::/64 dev veth165b4aa proto kernel metric 256 pref medium +fe80::/64 dev veth8c376e6 proto kernel metric 256 pref medium +fe80::/64 dev veth73c775b proto kernel metric 256 pref medium +fe80::/64 dev veth56166e3 proto kernel metric 256 pref medium +fe80::/64 dev br-4efe811b8402 proto kernel metric 256 pref medium +fe80::/64 dev veth14193e1 proto kernel metric 256 pref medium +fe80::/64 dev vethe9fcee3 proto kernel metric 256 pref medium +fe80::/64 dev veth71028db proto kernel metric 256 pref medium +fe80::/64 dev br-bca9ecea74f1 proto kernel metric 256 pref medium +fe80::/64 dev vethe123758 proto kernel metric 256 pref medium +fe80::/64 dev br-a35cbfd37253 proto kernel metric 256 pref medium +fe80::/64 dev veth8d698e0 proto kernel metric 256 pref medium +fe80::/64 dev br-71bcae37092b proto kernel metric 256 pref medium +fe80::/64 dev vethd87ba34 proto kernel metric 256 pref medium +fe80::/64 dev veth1bece5c proto kernel metric 256 pref medium +fe80::/64 dev br-c50ccbc5e464 proto kernel metric 256 pref medium +fe80::/64 dev veth6091231 proto kernel metric 256 pref medium +fe80::/64 dev veth502e53b proto kernel metric 256 pref medium +fe80::/64 dev vethe6c6432 proto kernel metric 256 pref medium +fe80::/64 dev br-618855db6757 proto kernel metric 256 pref medium +fe80::/64 dev veth1f1f066 proto kernel metric 256 pref medium +fe80::/64 dev veth26a5545 proto kernel metric 256 pref medium +fe80::/64 dev vethe488043 proto kernel metric 256 pref medium +fe80::/64 dev br-28584d053169 proto kernel metric 256 pref medium +fe80::/64 dev veth551b749 proto kernel metric 256 pref medium +fe80::/64 dev veth15eb212 proto kernel metric 256 pref medium +fe80::/64 dev br-4ecbee63f4b0 proto kernel metric 256 pref medium +fe80::/64 dev veth41b1943 proto kernel metric 256 pref medium +fe80::/64 dev vethc2b3531 proto kernel metric 256 pref medium +fe80::/64 dev veth62aa43d proto kernel metric 256 pref medium +fe80::/64 dev br-8200b83f03e9 proto kernel metric 256 pref medium +fe80::/64 dev veth081c8bf proto kernel metric 256 pref medium +fe80::/64 dev br-9891a93b5e9c proto kernel metric 256 pref medium +fe80::/64 dev enp4s0 proto kernel metric 1024 pref medium +default via fe80::3e8c:f8ff:fef4:2590 dev enp4s0 proto ra metric 100 pref medium +local ::1 dev lo table local proto kernel metric 0 pref medium +anycast 2603:7080:7500:1279:: dev enp4s0 table local proto kernel metric 0 pref medium +local 2603:7080:7500:1279:75aa:d64d:4cf0:8f10 dev enp4s0 table local proto kernel metric 0 pref medium +anycast fd00:1:: dev br-5a9988b94efd table local proto kernel metric 0 pref medium +local fd00:1::1 dev br-5a9988b94efd table local proto kernel metric 0 pref medium +anycast fd00:3:: dev br-9aea00f5a485 table local proto kernel metric 0 pref medium +local fd00:3::1 dev br-9aea00f5a485 table local proto kernel metric 0 pref medium +anycast fd00:4:: dev br-f63e542ab28c table local proto kernel metric 0 pref medium +local fd00:4::1 dev br-f63e542ab28c table local proto kernel metric 0 pref medium +anycast fd00:9:: dev br-8200b83f03e9 table local proto kernel metric 0 pref medium +local fd00:9::1 dev br-8200b83f03e9 table local proto kernel metric 0 pref medium +anycast fd00:10:: dev br-bca9ecea74f1 table local proto kernel metric 0 pref medium +local fd00:10::1 dev br-bca9ecea74f1 table local proto kernel metric 0 pref medium +anycast fd00:16:: dev br-618855db6757 table local proto kernel metric 0 pref medium +local fd00:16::1 dev br-618855db6757 table local proto kernel metric 0 pref medium +anycast fd00:17:: dev br-28584d053169 table local proto kernel metric 0 pref medium +local fd00:17::1 dev br-28584d053169 table local proto kernel metric 0 pref medium +anycast fd00:18:: dev br-4ecbee63f4b0 table local proto kernel metric 0 pref medium +local fd00:18::1 dev br-4ecbee63f4b0 table local proto kernel metric 0 pref medium +anycast fd00:20:: dev br-527a7cd84ff6 table local proto kernel metric 0 pref medium +local fd00:20::1 dev br-527a7cd84ff6 table local proto kernel metric 0 pref medium +anycast fd00:24:: dev br-f9343d8383ac table local proto kernel metric 0 pref medium +local fd00:24::1 dev br-f9343d8383ac table local proto kernel metric 0 pref medium +anycast fd00:26:: dev br-263c8508de6f table local proto kernel metric 0 pref medium +local fd00:26::1 dev br-263c8508de6f table local proto kernel metric 0 pref medium +anycast fd00:27:: dev br-87e08370adff table local proto kernel metric 0 pref medium +local fd00:27::1 dev br-87e08370adff table local proto kernel metric 0 pref medium +anycast fd00:28:: dev br-ed1bb1cc9d39 table local proto kernel metric 0 pref medium +local fd00:28::1 dev br-ed1bb1cc9d39 table local proto kernel metric 0 pref medium +anycast fd00:29:: dev br-f10b040acc46 table local proto kernel metric 0 pref medium +local fd00:29::1 dev br-f10b040acc46 table local proto kernel metric 0 pref medium +anycast fd00:30:: dev br-dc41f9983a47 table local proto kernel metric 0 pref medium +local fd00:30::1 dev br-dc41f9983a47 table local proto kernel metric 0 pref medium +anycast fd00:31:: dev br-f3f5c7cfa74e table local proto kernel metric 0 pref medium +local fd00:31::1 dev br-f3f5c7cfa74e table local proto kernel metric 0 pref medium +anycast fd00:40:: dev br-8b881e367322 table local proto kernel metric 0 pref medium +local fd00:40::1 dev br-8b881e367322 table local proto kernel metric 0 pref medium +anycast fd00:41:: dev br-051bbcfc21d8 table local proto kernel metric 0 pref medium +local fd00:41::1 dev br-051bbcfc21d8 table local proto kernel metric 0 pref medium +anycast fe80:: dev enp4s0 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth605aea1 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethf79d976 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-5a9988b94efd table local proto kernel metric 0 pref medium +anycast fe80:: dev docker0 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-dee363d4e0c6 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethca028f8 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethf583175 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-e02b7c89a2d3 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-875fced92cd0 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-11ceaf8736f5 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth880eb86 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethee5b2b0 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth55cb2ec table local proto kernel metric 0 pref medium +anycast fe80:: dev veth766794b table local proto kernel metric 0 pref medium +anycast fe80:: dev veth38ae454 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethbe8f01c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth563d3b9 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-b7976abb6e55 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-68e2ed0ec6ac table local proto kernel metric 0 pref medium +anycast fe80:: dev veth7e2863d table local proto kernel metric 0 pref medium +anycast fe80:: dev br-cfae917b997e table local proto kernel metric 0 pref medium +anycast fe80:: dev br-9aea00f5a485 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth4216565 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2edd7d6 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethcae9493 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth426f4c7 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-ecba5adea6ea table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2d01312 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth8c83268 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth362d4c6 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth6f6f074 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth99bb318 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-d89dcf384c2a table local proto kernel metric 0 pref medium +anycast fe80:: dev br-8b881e367322 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth4ecfbb5 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethc0ef06c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth46248ff table local proto kernel metric 0 pref medium +anycast fe80:: dev vethaeee758 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethbb40f36 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-2d50ca7a8f5d table local proto kernel metric 0 pref medium +anycast fe80:: dev br-6caddd639b0b table local proto kernel metric 0 pref medium +anycast fe80:: dev br-f63e542ab28c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth04a7fbe table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2906c94 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth28958e3 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth1f2dc82 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-f10b040acc46 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethd88eb9b table local proto kernel metric 0 pref medium +anycast fe80:: dev br-dc41f9983a47 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth6594ba0 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2273529 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-eeee48934935 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth160d05f table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2bde217 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth80da983 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-ed1bb1cc9d39 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth9e2bcbb table local proto kernel metric 0 pref medium +anycast fe80:: dev veth370f490 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-87e08370adff table local proto kernel metric 0 pref medium +anycast fe80:: dev veth0477205 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethd14eb7e table local proto kernel metric 0 pref medium +anycast fe80:: dev veth3c1861e table local proto kernel metric 0 pref medium +anycast fe80:: dev vetha548a1a table local proto kernel metric 0 pref medium +anycast fe80:: dev vethca0ee65 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethf48aa55 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-527a7cd84ff6 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-f3f5c7cfa74e table local proto kernel metric 0 pref medium +anycast fe80:: dev veth32d5964 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-03ee8c9e4bea table local proto kernel metric 0 pref medium +anycast fe80:: dev veth4692f5c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth79ea708 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth7d6d658 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth9cc2ad8 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth6232cb7 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth762d601 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth337e2fd table local proto kernel metric 0 pref medium +anycast fe80:: dev veth335556c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth4ba8ed1 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-fea0382ed10b table local proto kernel metric 0 pref medium +anycast fe80:: dev vethf5f3d34 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth448a204 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-263c8508de6f table local proto kernel metric 0 pref medium +anycast fe80:: dev veth4a87e58 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethe969d14 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth0e4600c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth21e2097 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth30db54a table local proto kernel metric 0 pref medium +anycast fe80:: dev vethacfaa19 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-f9343d8383ac table local proto kernel metric 0 pref medium +anycast fe80:: dev br-59ca07fbf200 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth6b9d50d table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2dd045f table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2445f8e table local proto kernel metric 0 pref medium +anycast fe80:: dev veth58179de table local proto kernel metric 0 pref medium +anycast fe80:: dev veth368657c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth5f6906b table local proto kernel metric 0 pref medium +anycast fe80:: dev veth165b4aa table local proto kernel metric 0 pref medium +anycast fe80:: dev veth53fd806 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-051bbcfc21d8 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth8c376e6 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth2b9371a table local proto kernel metric 0 pref medium +anycast fe80:: dev veth56166e3 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth73c775b table local proto kernel metric 0 pref medium +anycast fe80:: dev br-4efe811b8402 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethe9fcee3 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth14193e1 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth71028db table local proto kernel metric 0 pref medium +anycast fe80:: dev br-bca9ecea74f1 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-a35cbfd37253 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth8d698e0 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethe123758 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-71bcae37092b table local proto kernel metric 0 pref medium +anycast fe80:: dev veth6091231 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethd87ba34 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth1bece5c table local proto kernel metric 0 pref medium +anycast fe80:: dev br-c50ccbc5e464 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth26a5545 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth502e53b table local proto kernel metric 0 pref medium +anycast fe80:: dev vethe6c6432 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-618855db6757 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth1f1f066 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethe488043 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-28584d053169 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth41b1943 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth551b749 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth15eb212 table local proto kernel metric 0 pref medium +anycast fe80:: dev vethc2b3531 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-4ecbee63f4b0 table local proto kernel metric 0 pref medium +anycast fe80:: dev veth62aa43d table local proto kernel metric 0 pref medium +anycast fe80:: dev br-8200b83f03e9 table local proto kernel metric 0 pref medium +anycast fe80:: dev br-9891a93b5e9c table local proto kernel metric 0 pref medium +anycast fe80:: dev veth081c8bf table local proto kernel metric 0 pref medium +local fe80::63:73ff:fede:43f9 dev vethbe8f01c table local proto kernel metric 0 pref medium +local fe80::424:9bff:fe91:317e dev veth762d601 table local proto kernel metric 0 pref medium +local fe80::443:71ff:feac:bdfb dev vethe9fcee3 table local proto kernel metric 0 pref medium +local fe80::464:d7ff:fea1:a54a dev veth2dd045f table local proto kernel metric 0 pref medium +local fe80::4a3:bbff:fea9:df98 dev veth99bb318 table local proto kernel metric 0 pref medium +local fe80::869:a1ff:fee9:63a6 dev veth2273529 table local proto kernel metric 0 pref medium +local fe80::c1f:b5ff:fe0a:c936 dev vethca0ee65 table local proto kernel metric 0 pref medium +local fe80::e6f:317c:ee82:e5e dev enp4s0 table local proto kernel metric 0 pref medium +local fe80::1058:f8ff:fe69:21bb dev br-68e2ed0ec6ac table local proto kernel metric 0 pref medium +local fe80::18c7:26ff:fe8b:bea4 dev veth46248ff table local proto kernel metric 0 pref medium +local fe80::1c13:3ff:fe18:c9a4 dev veth2906c94 table local proto kernel metric 0 pref medium +local fe80::1c6e:c5ff:fe94:c515 dev veth3c1861e table local proto kernel metric 0 pref medium +local fe80::2095:66ff:fe78:518e dev veth4ba8ed1 table local proto kernel metric 0 pref medium +local fe80::24de:7fff:fef9:e7af dev veth2bde217 table local proto kernel metric 0 pref medium +local fe80::2844:4dff:fec7:1613 dev vethcae9493 table local proto kernel metric 0 pref medium +local fe80::286c:a2ff:fe49:5772 dev br-71bcae37092b table local proto kernel metric 0 pref medium +local fe80::28b8:bdff:fe81:370f dev veth55cb2ec table local proto kernel metric 0 pref medium +local fe80::28df:aff:fe50:a6d2 dev br-f63e542ab28c table local proto kernel metric 0 pref medium +local fe80::2c30:58ff:fe89:f9d2 dev br-e02b7c89a2d3 table local proto kernel metric 0 pref medium +local fe80::2c60:58ff:fe86:30d3 dev veth4692f5c table local proto kernel metric 0 pref medium +local fe80::303f:6ff:fe24:1fa6 dev vethe969d14 table local proto kernel metric 0 pref medium +local fe80::30d4:6bff:fe54:5fb3 dev br-ed1bb1cc9d39 table local proto kernel metric 0 pref medium +local fe80::30f1:2cff:feac:90cb dev veth1bece5c table local proto kernel metric 0 pref medium +local fe80::3441:76ff:fe3f:f8d1 dev veth766794b table local proto kernel metric 0 pref medium +local fe80::3c0c:90ff:fe10:332f dev veth2edd7d6 table local proto kernel metric 0 pref medium +local fe80::3c1f:f9ff:fe36:4122 dev veth081c8bf table local proto kernel metric 0 pref medium +local fe80::3c6e:51ff:fe48:e037 dev vetha548a1a table local proto kernel metric 0 pref medium +local fe80::40f5:bfff:fed7:8296 dev br-87e08370adff table local proto kernel metric 0 pref medium +local fe80::4421:95ff:fe94:d7c5 dev br-bca9ecea74f1 table local proto kernel metric 0 pref medium +local fe80::4463:6dff:fe29:79d5 dev veth4216565 table local proto kernel metric 0 pref medium +local fe80::4482:41ff:feb3:9c74 dev br-527a7cd84ff6 table local proto kernel metric 0 pref medium +local fe80::44ee:8eff:fe1a:bcf4 dev vethe123758 table local proto kernel metric 0 pref medium +local fe80::48e9:9ff:fea5:2c72 dev docker0 table local proto kernel metric 0 pref medium +local fe80::48ec:7cff:fe17:27bc dev veth0477205 table local proto kernel metric 0 pref medium +local fe80::4c52:c7ff:fe49:3b0e dev vethf79d976 table local proto kernel metric 0 pref medium +local fe80::4c63:fcff:fe4c:7d6a dev br-f10b040acc46 table local proto kernel metric 0 pref medium +local fe80::4c80:8fff:feec:15d4 dev br-8200b83f03e9 table local proto kernel metric 0 pref medium +local fe80::502a:e5ff:fe5c:d5fa dev veth5f6906b table local proto kernel metric 0 pref medium +local fe80::5030:beff:fead:c50a dev veth9e2bcbb table local proto kernel metric 0 pref medium +local fe80::50f3:d6ff:fedd:1cd3 dev veth160d05f table local proto kernel metric 0 pref medium +local fe80::542a:61ff:fe38:307c dev veth8c376e6 table local proto kernel metric 0 pref medium +local fe80::5455:fbff:fec6:391e dev veth2d01312 table local proto kernel metric 0 pref medium +local fe80::548d:1eff:fe36:12c5 dev br-2d50ca7a8f5d table local proto kernel metric 0 pref medium +local fe80::548d:f8ff:fef4:c0db dev veth880eb86 table local proto kernel metric 0 pref medium +local fe80::5852:d0ff:fe9e:545 dev vethc0ef06c table local proto kernel metric 0 pref medium +local fe80::5c80:75ff:feff:22dd dev veth15eb212 table local proto kernel metric 0 pref medium +local fe80::60fc:94ff:fe7d:a58f dev br-cfae917b997e table local proto kernel metric 0 pref medium +local fe80::6486:bfff:fee7:22df dev vethee5b2b0 table local proto kernel metric 0 pref medium +local fe80::64fe:2aff:fed1:1e41 dev veth6232cb7 table local proto kernel metric 0 pref medium +local fe80::68b1:bff:fee2:9d17 dev br-9aea00f5a485 table local proto kernel metric 0 pref medium +local fe80::6c50:96ff:fe29:70a6 dev veth9cc2ad8 table local proto kernel metric 0 pref medium +local fe80::6c7e:7fff:fe2a:bf1d dev br-875fced92cd0 table local proto kernel metric 0 pref medium +local fe80::6c9c:c1ff:fe95:888e dev br-fea0382ed10b table local proto kernel metric 0 pref medium +local fe80::6cb5:cff:fea2:a675 dev veth58179de table local proto kernel metric 0 pref medium +local fe80::7069:c0ff:febd:1d88 dev vethe488043 table local proto kernel metric 0 pref medium +local fe80::7462:7dff:fe8f:115d dev veth73c775b table local proto kernel metric 0 pref medium +local fe80::74c2:12ff:fe72:7875 dev veth426f4c7 table local proto kernel metric 0 pref medium +local fe80::78cf:3dff:fe72:528b dev br-dc41f9983a47 table local proto kernel metric 0 pref medium +local fe80::7c42:75ff:fe80:5953 dev veth551b749 table local proto kernel metric 0 pref medium +local fe80::7c73:c8ff:fe04:3dd2 dev br-28584d053169 table local proto kernel metric 0 pref medium +local fe80::7cdd:44ff:fe90:1f77 dev veth53fd806 table local proto kernel metric 0 pref medium +local fe80::805a:47ff:fe53:cfcd dev vethf48aa55 table local proto kernel metric 0 pref medium +local fe80::80a3:1eff:fe61:4f10 dev veth32d5964 table local proto kernel metric 0 pref medium +local fe80::80fb:8fff:fead:2208 dev veth7d6d658 table local proto kernel metric 0 pref medium +local fe80::84bf:b2ff:fec4:ea7 dev vethd14eb7e table local proto kernel metric 0 pref medium +local fe80::8832:52ff:fee4:8a93 dev veth8c83268 table local proto kernel metric 0 pref medium +local fe80::888d:45ff:feeb:4a48 dev veth26a5545 table local proto kernel metric 0 pref medium +local fe80::9091:4aff:feee:8942 dev veth6594ba0 table local proto kernel metric 0 pref medium +local fe80::90da:e3ff:fefa:7464 dev veth1f1f066 table local proto kernel metric 0 pref medium +local fe80::90ed:30ff:fe51:2b5a dev veth335556c table local proto kernel metric 0 pref medium +local fe80::90ed:f4ff:feed:a749 dev vethacfaa19 table local proto kernel metric 0 pref medium +local fe80::90ff:b0ff:fe58:21fb dev veth448a204 table local proto kernel metric 0 pref medium +local fe80::9879:f6ff:fe6d:57be dev br-c50ccbc5e464 table local proto kernel metric 0 pref medium +local fe80::a047:6eff:fe8b:1f48 dev veth502e53b table local proto kernel metric 0 pref medium +local fe80::a065:bfff:fea5:34 dev veth368657c table local proto kernel metric 0 pref medium +local fe80::a065:eaff:fef3:532d dev br-11ceaf8736f5 table local proto kernel metric 0 pref medium +local fe80::a0eb:3bff:fe66:df68 dev veth80da983 table local proto kernel metric 0 pref medium +local fe80::a4a0:13ff:fe7f:7c8d dev vethd88eb9b table local proto kernel metric 0 pref medium +local fe80::a4ab:b9ff:fe3e:af94 dev veth71028db table local proto kernel metric 0 pref medium +local fe80::a843:f3ff:fedb:ef64 dev br-5a9988b94efd table local proto kernel metric 0 pref medium +local fe80::a8fe:4ff:fe1f:d81a dev br-b7976abb6e55 table local proto kernel metric 0 pref medium +local fe80::ac43:75ff:fefa:cbfd dev veth79ea708 table local proto kernel metric 0 pref medium +local fe80::ac5e:9cff:fe45:ec06 dev veth7e2863d table local proto kernel metric 0 pref medium +local fe80::ac67:76ff:fe15:1676 dev veth6f6f074 table local proto kernel metric 0 pref medium +local fe80::ac86:86ff:fe64:f2d4 dev br-eeee48934935 table local proto kernel metric 0 pref medium +local fe80::aca7:12ff:fe18:d1b9 dev br-a35cbfd37253 table local proto kernel metric 0 pref medium +local fe80::aca7:68ff:fe42:e77 dev veth14193e1 table local proto kernel metric 0 pref medium +local fe80::aca8:14ff:feee:df7c dev br-618855db6757 table local proto kernel metric 0 pref medium +local fe80::acc5:57ff:fece:3b51 dev veth62aa43d table local proto kernel metric 0 pref medium +local fe80::b441:acff:fe66:5981 dev veth0e4600c table local proto kernel metric 0 pref medium +local fe80::b471:19ff:fe52:ff1 dev br-d89dcf384c2a table local proto kernel metric 0 pref medium +local fe80::b48b:1aff:fe71:36d3 dev veth21e2097 table local proto kernel metric 0 pref medium +local fe80::b4d5:57ff:fe87:83f9 dev vethc2b3531 table local proto kernel metric 0 pref medium +local fe80::b86e:58ff:fef5:85f6 dev br-dee363d4e0c6 table local proto kernel metric 0 pref medium +local fe80::bc01:62ff:fe13:b616 dev veth41b1943 table local proto kernel metric 0 pref medium +local fe80::c000:96ff:fe7d:11a8 dev vethbb40f36 table local proto kernel metric 0 pref medium +local fe80::c006:5ff:fea6:b743 dev vethf5f3d34 table local proto kernel metric 0 pref medium +local fe80::c030:54ff:fe2d:eb36 dev br-8b881e367322 table local proto kernel metric 0 pref medium +local fe80::c090:5eff:fed8:d1cc dev veth370f490 table local proto kernel metric 0 pref medium +local fe80::c0da:5bff:fea4:92dc dev br-6caddd639b0b table local proto kernel metric 0 pref medium +local fe80::c85e:5aff:feaa:f812 dev veth165b4aa table local proto kernel metric 0 pref medium +local fe80::c899:1fff:fe27:7f66 dev veth4a87e58 table local proto kernel metric 0 pref medium +local fe80::c8b0:56ff:fe7e:bc48 dev br-f9343d8383ac table local proto kernel metric 0 pref medium +local fe80::cc80:50ff:feb5:67b dev veth2445f8e table local proto kernel metric 0 pref medium +local fe80::ccd1:f1ff:fe0f:c699 dev vethca028f8 table local proto kernel metric 0 pref medium +local fe80::d037:82ff:fee7:27c5 dev veth28958e3 table local proto kernel metric 0 pref medium +local fe80::d037:e3ff:fe79:1a5f dev veth563d3b9 table local proto kernel metric 0 pref medium +local fe80::d44c:3bff:fe3b:934f dev veth2b9371a table local proto kernel metric 0 pref medium +local fe80::d44d:feff:fe18:d291 dev veth6091231 table local proto kernel metric 0 pref medium +local fe80::d457:2bff:fe72:23f dev br-051bbcfc21d8 table local proto kernel metric 0 pref medium +local fe80::d45e:64ff:fe90:37e7 dev veth6b9d50d table local proto kernel metric 0 pref medium +local fe80::d847:aaff:fe89:3f9 dev veth4ecfbb5 table local proto kernel metric 0 pref medium +local fe80::d86b:4fff:fec2:50e0 dev veth8d698e0 table local proto kernel metric 0 pref medium +local fe80::d87e:fbff:fec6:5aeb dev br-4efe811b8402 table local proto kernel metric 0 pref medium +local fe80::d8c8:aaff:fe63:6af9 dev br-263c8508de6f table local proto kernel metric 0 pref medium +local fe80::dc0f:5dff:fec4:7c53 dev veth56166e3 table local proto kernel metric 0 pref medium +local fe80::e0a6:32ff:fe63:9f8d dev br-9891a93b5e9c table local proto kernel metric 0 pref medium +local fe80::e447:bff:feea:1f61 dev vethe6c6432 table local proto kernel metric 0 pref medium +local fe80::e46c:23ff:fec3:3ab9 dev br-4ecbee63f4b0 table local proto kernel metric 0 pref medium +local fe80::e488:ccff:fe2f:e545 dev veth04a7fbe table local proto kernel metric 0 pref medium +local fe80::e49e:23ff:feb7:932d dev veth1f2dc82 table local proto kernel metric 0 pref medium +local fe80::e807:c7ff:fee0:f12d dev vethaeee758 table local proto kernel metric 0 pref medium +local fe80::e855:acff:fef2:5aa3 dev veth30db54a table local proto kernel metric 0 pref medium +local fe80::e87f:edff:fe10:e8d6 dev vethd87ba34 table local proto kernel metric 0 pref medium +local fe80::ec3f:78ff:fe38:75b9 dev veth605aea1 table local proto kernel metric 0 pref medium +local fe80::f01a:4eff:fef2:9942 dev veth38ae454 table local proto kernel metric 0 pref medium +local fe80::f01f:e5ff:fe2b:ec80 dev br-03ee8c9e4bea table local proto kernel metric 0 pref medium +local fe80::f038:dbff:fefd:87d3 dev veth362d4c6 table local proto kernel metric 0 pref medium +local fe80::f0bf:5ff:fee7:2b1e dev veth337e2fd table local proto kernel metric 0 pref medium +local fe80::f0c3:fff:fe8b:74e3 dev vethf583175 table local proto kernel metric 0 pref medium +local fe80::f4a6:b6ff:fe24:af71 dev br-f3f5c7cfa74e table local proto kernel metric 0 pref medium +local fe80::f8d4:eff:fe6e:19a9 dev br-ecba5adea6ea table local proto kernel metric 0 pref medium +local fe80::fc79:ccff:fefd:20d7 dev br-59ca07fbf200 table local proto kernel metric 0 pref medium +multicast ff00::/8 dev enp4s0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev wg0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethf79d976 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev docker0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth605aea1 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-5a9988b94efd table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethca028f8 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-e02b7c89a2d3 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-dee363d4e0c6 table local proto kernel metric 256 linkdown pref medium +multicast ff00::/8 dev br-875fced92cd0 table local proto kernel metric 256 linkdown pref medium +multicast ff00::/8 dev vethf583175 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-11ceaf8736f5 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth880eb86 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth7e2863d table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth55cb2ec table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethee5b2b0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-9aea00f5a485 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethbe8f01c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-b7976abb6e55 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2edd7d6 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-68e2ed0ec6ac table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth38ae454 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-cfae917b997e table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth563d3b9 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-ecba5adea6ea table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth766794b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-d89dcf384c2a table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth426f4c7 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth362d4c6 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2d01312 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth4ecfbb5 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth99bb318 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethcae9493 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth8c83268 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth6f6f074 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth4216565 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth46248ff table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethc0ef06c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethaeee758 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-f63e542ab28c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-8b881e367322 table local proto kernel metric 256 linkdown pref medium +multicast ff00::/8 dev veth2906c94 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-6caddd639b0b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethbb40f36 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-2d50ca7a8f5d table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth6594ba0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-dc41f9983a47 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth28958e3 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth1f2dc82 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth04a7fbe table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2273529 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth160d05f table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-eeee48934935 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethd88eb9b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-f10b040acc46 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth80da983 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2bde217 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth9e2bcbb table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth0477205 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-87e08370adff table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth370f490 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-ed1bb1cc9d39 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethca0ee65 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth4692f5c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethd14eb7e table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-527a7cd84ff6 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth3c1861e table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-03ee8c9e4bea table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth32d5964 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vetha548a1a table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth762d601 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethf48aa55 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethf5f3d34 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-f3f5c7cfa74e table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth335556c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth9cc2ad8 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth6232cb7 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth337e2fd table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth7d6d658 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth448a204 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth79ea708 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth4ba8ed1 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-263c8508de6f table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth4a87e58 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-fea0382ed10b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethacfaa19 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth30db54a table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethe969d14 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth0e4600c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth21e2097 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-f9343d8383ac table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth58179de table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-59ca07fbf200 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth6b9d50d table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2445f8e table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth368657c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth5f6906b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2dd045f table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth53fd806 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth2b9371a table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-051bbcfc21d8 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth165b4aa table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth8c376e6 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth73c775b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth56166e3 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-4efe811b8402 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth14193e1 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethe9fcee3 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth71028db table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-bca9ecea74f1 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethe123758 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-a35cbfd37253 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth8d698e0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-71bcae37092b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethd87ba34 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth1bece5c table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-c50ccbc5e464 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth6091231 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth502e53b table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethe6c6432 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-618855db6757 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth1f1f066 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth26a5545 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethe488043 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-28584d053169 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth551b749 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth15eb212 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-4ecbee63f4b0 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth41b1943 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev vethc2b3531 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth62aa43d table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-8200b83f03e9 table local proto kernel metric 256 pref medium +multicast ff00::/8 dev veth081c8bf table local proto kernel metric 256 pref medium +multicast ff00::/8 dev br-9891a93b5e9c table local proto kernel metric 256 pref medium + +=== IPv4 rules === +0: from all lookup local +32765: from 10.98.0.2 lookup wg0 +32766: from all lookup main +32767: from all lookup default + +=== IPv6 rules === +0: from all lookup local +32766: from all lookup main + +=== namespaces === + +=== resolvers === +# Generated by NetworkManager +search lan +nameserver 192.168.10.1 +nameserver fe80::3e8c:f8ff:fef4:2590%enp4s0 + +=== observed public IPv4 === +74.67.173.56 + +=== observed public IPv6 === +2603:7080:7500:1279:75aa:d64d:4cf0:8f10 diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/nftables-ruleset.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/nftables-ruleset.txt new file mode 100644 index 0000000..5650b03 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/nftables-ruleset.txt @@ -0,0 +1,702 @@ +# Warning: table ip filter is managed by iptables-nft, do not touch! +table ip filter { # handle 1 + chain INPUT { # handle 1 + type filter hook input priority filter; policy accept; + } + + chain FORWARD { # handle 2 + type filter hook forward priority filter; policy accept; + counter packets 1393654 bytes 658721511 jump DOCKER-USER # handle 170 + counter packets 1393654 bytes 658721511 jump DOCKER-FORWARD # handle 9 + } + + chain OUTPUT { # handle 3 + type filter hook output priority filter; policy accept; + } + + chain DOCKER { # handle 4 + ip daddr 172.31.0.2 iifname != "br-9891a93b5e9c" oifname "br-9891a93b5e9c" tcp dport 8945 counter packets 0 bytes 0 accept # handle 463 + ip daddr 10.9.0.2 iifname != "br-8200b83f03e9" oifname "br-8200b83f03e9" tcp dport 8096 counter packets 0 bytes 0 accept # handle 458 + ip daddr 10.17.0.2 iifname != "br-28584d053169" oifname "br-28584d053169" tcp dport 9980 counter packets 0 bytes 0 accept # handle 446 + ip daddr 10.1.0.6 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 4533 counter packets 0 bytes 0 accept # handle 441 + ip daddr 10.19.0.3 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 5030 counter packets 0 bytes 0 accept # handle 435 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 9696 counter packets 0 bytes 0 accept # handle 434 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8989 counter packets 0 bytes 0 accept # handle 433 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8888 counter packets 0 bytes 0 accept # handle 432 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8787 counter packets 0 bytes 0 accept # handle 431 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8265 counter packets 0 bytes 0 accept # handle 430 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8191 counter packets 0 bytes 0 accept # handle 429 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 8080 counter packets 0 bytes 0 accept # handle 428 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 7878 counter packets 0 bytes 0 accept # handle 427 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 6767 counter packets 0 bytes 0 accept # handle 426 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 5075 counter packets 0 bytes 0 accept # handle 425 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 5055 counter packets 0 bytes 0 accept # handle 424 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" tcp dport 5000 counter packets 0 bytes 0 accept # handle 423 + ip daddr 172.30.0.3 iifname != "br-71bcae37092b" oifname "br-71bcae37092b" tcp dport 8080 counter packets 0 bytes 0 accept # handle 418 + ip daddr 10.13.0.2 iifname != "br-a35cbfd37253" oifname "br-a35cbfd37253" tcp dport 8000 counter packets 0 bytes 0 accept # handle 413 + ip daddr 10.10.0.2 iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" tcp dport 443 counter packets 0 bytes 0 accept # handle 408 + ip daddr 10.10.0.2 iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" tcp dport 80 counter packets 0 bytes 0 accept # handle 407 + ip daddr 10.99.0.16 iifname != "br-e02b7c89a2d3" oifname "br-e02b7c89a2d3" tcp dport 9000 counter packets 0 bytes 0 accept # handle 402 + ip daddr 10.1.0.5 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 8080 counter packets 0 bytes 0 accept # handle 397 + ip daddr 10.1.0.3 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 80 counter packets 0 bytes 0 accept # handle 391 + ip daddr 10.24.0.2 iifname != "br-f9343d8383ac" oifname "br-f9343d8383ac" tcp dport 8080 counter packets 0 bytes 0 accept # handle 386 + ip daddr 172.26.0.5 iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" tcp dport 80 counter packets 0 bytes 0 accept # handle 381 + ip daddr 172.26.0.4 iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" udp dport 10009 counter packets 0 bytes 0 accept # handle 380 + ip daddr 172.26.0.4 iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" tcp dport 9090 counter packets 0 bytes 0 accept # handle 379 + ip daddr 172.26.0.3 iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" tcp dport 8888 counter packets 0 bytes 0 accept # handle 378 + ip daddr 172.26.0.2 iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" tcp dport 5280 counter packets 0 bytes 0 accept # handle 377 + ip daddr 10.26.0.2 iifname != "br-263c8508de6f" oifname "br-263c8508de6f" tcp dport 6167 counter packets 0 bytes 0 accept # handle 372 + ip daddr 10.31.0.5 iifname != "br-f3f5c7cfa74e" oifname "br-f3f5c7cfa74e" tcp dport 4000 counter packets 0 bytes 0 accept # handle 367 + ip daddr 10.31.0.4 iifname != "br-f3f5c7cfa74e" oifname "br-f3f5c7cfa74e" tcp dport 3000 counter packets 0 bytes 0 accept # handle 366 + ip daddr 10.31.0.2 iifname != "br-f3f5c7cfa74e" oifname "br-f3f5c7cfa74e" tcp dport 9200 counter packets 0 bytes 0 accept # handle 363 + ip daddr 172.28.0.5 iifname != "br-03ee8c9e4bea" oifname "br-03ee8c9e4bea" tcp dport 8536 counter packets 0 bytes 0 accept # handle 358 + ip daddr 172.28.0.5 iifname != "br-03ee8c9e4bea" oifname "br-03ee8c9e4bea" tcp dport 1236 counter packets 0 bytes 0 accept # handle 357 + ip daddr 10.20.0.2 iifname != "br-527a7cd84ff6" oifname "br-527a7cd84ff6" tcp dport 8097 counter packets 0 bytes 0 accept # handle 352 + ip daddr 10.20.0.2 iifname != "br-527a7cd84ff6" oifname "br-527a7cd84ff6" tcp dport 6697 counter packets 1 bytes 52 accept # handle 351 + ip daddr 10.20.0.2 iifname != "br-527a7cd84ff6" oifname "br-527a7cd84ff6" tcp dport 6667 counter packets 0 bytes 0 accept # handle 350 + ip daddr 10.28.0.3 iifname != "br-ed1bb1cc9d39" oifname "br-ed1bb1cc9d39" tcp dport 8080 counter packets 0 bytes 0 accept # handle 345 + ip daddr 10.27.0.2 iifname != "br-87e08370adff" oifname "br-87e08370adff" tcp dport 3000 counter packets 0 bytes 0 accept # handle 340 + ip daddr 10.27.0.2 iifname != "br-87e08370adff" oifname "br-87e08370adff" tcp dport 2244 counter packets 0 bytes 0 accept # handle 339 + ip daddr 10.29.0.3 iifname != "br-f10b040acc46" oifname "br-f10b040acc46" tcp dport 3000 counter packets 0 bytes 0 accept # handle 332 + ip daddr 172.24.0.2 iifname != "br-eeee48934935" oifname "br-eeee48934935" tcp dport 7576 counter packets 0 bytes 0 accept # handle 326 + ip daddr 10.30.0.4 iifname != "br-dc41f9983a47" oifname "br-dc41f9983a47" tcp dport 80 counter packets 0 bytes 0 accept # handle 321 + ip daddr 172.23.0.2 iifname != "br-2d50ca7a8f5d" oifname "br-2d50ca7a8f5d" tcp dport 8080 counter packets 0 bytes 0 accept # handle 316 + ip daddr 172.23.0.2 iifname != "br-2d50ca7a8f5d" oifname "br-2d50ca7a8f5d" tcp dport 1935 counter packets 0 bytes 0 accept # handle 315 + ip daddr 172.22.0.2 iifname != "br-6caddd639b0b" oifname "br-6caddd639b0b" tcp dport 8765 counter packets 0 bytes 0 accept # handle 310 + ip daddr 10.4.0.2 iifname != "br-f63e542ab28c" oifname "br-f63e542ab28c" tcp dport 80 counter packets 0 bytes 0 accept # handle 299 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" tcp dport 993 counter packets 33 bytes 1948 accept # handle 294 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" tcp dport 465 counter packets 23 bytes 1252 accept # handle 293 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" tcp dport 443 counter packets 0 bytes 0 accept # handle 292 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" tcp dport 80 counter packets 0 bytes 0 accept # handle 291 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" tcp dport 25 counter packets 500 bytes 29920 accept # handle 290 + ip daddr 10.3.0.2 iifname != "br-9aea00f5a485" oifname "br-9aea00f5a485" tcp dport 8888 counter packets 0 bytes 0 accept # handle 271 + ip daddr 10.55.0.3 iifname != "br-11ceaf8736f5" oifname "br-11ceaf8736f5" tcp dport 7001 counter packets 0 bytes 0 accept # handle 207 + ip daddr 10.55.0.2 iifname != "br-11ceaf8736f5" oifname "br-11ceaf8736f5" tcp dport 3000 counter packets 0 bytes 0 accept # handle 199 + ip daddr 172.17.0.4 iifname != "docker0" oifname "docker0" tcp dport 5432 counter packets 0 bytes 0 accept # handle 183 + ip daddr 172.17.0.3 iifname != "docker0" oifname "docker0" tcp dport 3306 counter packets 0 bytes 0 accept # handle 177 + iifname != "br-11ceaf8736f5" oifname "br-11ceaf8736f5" counter packets 0 bytes 0 drop # handle 18 + iifname != "br-dee363d4e0c6" oifname "br-dee363d4e0c6" counter packets 0 bytes 0 drop # handle 26 + iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 73 + iifname != "br-e02b7c89a2d3" oifname "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 137 + iifname != "br-875fced92cd0" oifname "br-875fced92cd0" counter packets 0 bytes 0 drop # handle 153 + iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop # handle 172 + iifname != "br-9aea00f5a485" oifname "br-9aea00f5a485" counter packets 0 bytes 0 drop # handle 268 + iifname != "br-68e2ed0ec6ac" oifname "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 273 + iifname != "br-b7976abb6e55" oifname "br-b7976abb6e55" counter packets 0 bytes 0 drop # handle 277 + iifname != "br-cfae917b997e" oifname "br-cfae917b997e" counter packets 0 bytes 0 drop # handle 284 + iifname != "br-f63e542ab28c" oifname "br-f63e542ab28c" counter packets 0 bytes 0 drop # handle 296 + iifname != "br-8b881e367322" oifname "br-8b881e367322" counter packets 0 bytes 0 drop # handle 301 + iifname != "br-6caddd639b0b" oifname "br-6caddd639b0b" counter packets 0 bytes 0 drop # handle 307 + iifname != "br-2d50ca7a8f5d" oifname "br-2d50ca7a8f5d" counter packets 0 bytes 0 drop # handle 312 + iifname != "br-dc41f9983a47" oifname "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 318 + iifname != "br-eeee48934935" oifname "br-eeee48934935" counter packets 0 bytes 0 drop # handle 323 + iifname != "br-f10b040acc46" oifname "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 328 + iifname != "br-87e08370adff" oifname "br-87e08370adff" counter packets 0 bytes 0 drop # handle 334 + iifname != "br-ed1bb1cc9d39" oifname "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 342 + iifname != "br-527a7cd84ff6" oifname "br-527a7cd84ff6" counter packets 0 bytes 0 drop # handle 347 + iifname != "br-03ee8c9e4bea" oifname "br-03ee8c9e4bea" counter packets 0 bytes 0 drop # handle 354 + iifname != "br-f3f5c7cfa74e" oifname "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 360 + iifname != "br-263c8508de6f" oifname "br-263c8508de6f" counter packets 0 bytes 0 drop # handle 369 + iifname != "br-fea0382ed10b" oifname "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 374 + iifname != "br-f9343d8383ac" oifname "br-f9343d8383ac" counter packets 0 bytes 0 drop # handle 383 + iifname != "br-59ca07fbf200" oifname "br-59ca07fbf200" counter packets 0 bytes 0 drop # handle 388 + iifname != "br-051bbcfc21d8" oifname "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 393 + iifname != "br-4efe811b8402" oifname "br-4efe811b8402" counter packets 0 bytes 0 drop # handle 399 + iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" counter packets 0 bytes 0 drop # handle 404 + iifname != "br-a35cbfd37253" oifname "br-a35cbfd37253" counter packets 0 bytes 0 drop # handle 410 + iifname != "br-71bcae37092b" oifname "br-71bcae37092b" counter packets 0 bytes 0 drop # handle 415 + iifname != "br-c50ccbc5e464" oifname "br-c50ccbc5e464" counter packets 0 bytes 0 drop # handle 420 + iifname != "br-618855db6757" oifname "br-618855db6757" counter packets 0 bytes 0 drop # handle 437 + iifname != "br-28584d053169" oifname "br-28584d053169" counter packets 0 bytes 0 drop # handle 443 + iifname != "br-4ecbee63f4b0" oifname "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 448 + iifname != "br-8200b83f03e9" oifname "br-8200b83f03e9" counter packets 0 bytes 0 drop # handle 455 + iifname != "br-9891a93b5e9c" oifname "br-9891a93b5e9c" counter packets 0 bytes 0 drop # handle 460 + } + + chain DOCKER-FORWARD { # handle 5 + counter packets 1393654 bytes 658721511 jump DOCKER-CT # handle 12 + counter packets 811898 bytes 429419843 jump DOCKER-INTERNAL # handle 11 + counter packets 811898 bytes 429419843 jump DOCKER-BRIDGE # handle 10 + iifname "br-11ceaf8736f5" counter packets 3260 bytes 175561 accept # handle 17 + iifname "br-dee363d4e0c6" counter packets 0 bytes 0 accept # handle 25 + iifname "br-5a9988b94efd" counter packets 185 bytes 27196 accept # handle 72 + iifname "br-e02b7c89a2d3" counter packets 46734 bytes 8416975 accept # handle 136 + iifname "br-875fced92cd0" counter packets 0 bytes 0 accept # handle 152 + iifname "docker0" counter packets 0 bytes 0 accept # handle 171 + iifname "br-9aea00f5a485" counter packets 0 bytes 0 accept # handle 267 + iifname "br-68e2ed0ec6ac" counter packets 492 bytes 40183 accept # handle 272 + iifname "br-b7976abb6e55" counter packets 10668 bytes 5341503 accept # handle 276 + iifname "br-ecba5adea6ea" oifname "br-ecba5adea6ea" counter packets 0 bytes 0 accept # handle 282 + iifname "br-cfae917b997e" counter packets 43 bytes 4954 accept # handle 283 + iifname "br-d89dcf384c2a" oifname "br-d89dcf384c2a" counter packets 0 bytes 0 accept # handle 289 + iifname "br-f63e542ab28c" counter packets 0 bytes 0 accept # handle 295 + iifname "br-8b881e367322" counter packets 135 bytes 13854 accept # handle 300 + iifname "br-6caddd639b0b" counter packets 0 bytes 0 accept # handle 306 + iifname "br-2d50ca7a8f5d" counter packets 0 bytes 0 accept # handle 311 + iifname "br-dc41f9983a47" counter packets 0 bytes 0 accept # handle 317 + iifname "br-eeee48934935" counter packets 46 bytes 3053 accept # handle 322 + iifname "br-f10b040acc46" counter packets 1774 bytes 177630 accept # handle 327 + iifname "br-87e08370adff" counter packets 0 bytes 0 accept # handle 333 + iifname "br-ed1bb1cc9d39" counter packets 15 bytes 1717 accept # handle 341 + iifname "br-527a7cd84ff6" counter packets 3 bytes 132 accept # handle 346 + iifname "br-03ee8c9e4bea" counter packets 6089 bytes 353878 accept # handle 353 + iifname "br-f3f5c7cfa74e" counter packets 639 bytes 70250 accept # handle 359 + iifname "br-263c8508de6f" counter packets 24004 bytes 2190194 accept # handle 368 + iifname "br-fea0382ed10b" counter packets 134 bytes 30967 accept # handle 373 + iifname "br-f9343d8383ac" counter packets 0 bytes 0 accept # handle 382 + iifname "br-59ca07fbf200" counter packets 0 bytes 0 accept # handle 387 + iifname "br-051bbcfc21d8" counter packets 0 bytes 0 accept # handle 392 + iifname "br-4efe811b8402" counter packets 0 bytes 0 accept # handle 398 + iifname "br-bca9ecea74f1" counter packets 0 bytes 0 accept # handle 403 + iifname "br-a35cbfd37253" counter packets 0 bytes 0 accept # handle 409 + iifname "br-71bcae37092b" counter packets 0 bytes 0 accept # handle 414 + iifname "br-c50ccbc5e464" counter packets 637050 bytes 401364749 accept # handle 419 + iifname "br-618855db6757" counter packets 0 bytes 0 accept # handle 436 + iifname "br-28584d053169" counter packets 12 bytes 1526 accept # handle 442 + iifname "br-4ecbee63f4b0" counter packets 0 bytes 0 accept # handle 447 + iifname "br-8200b83f03e9" counter packets 52 bytes 6434 accept # handle 454 + iifname "br-9891a93b5e9c" counter packets 5916 bytes 322250 accept # handle 459 + } + + chain DOCKER-BRIDGE { # handle 6 + oifname "br-11ceaf8736f5" counter packets 0 bytes 0 jump DOCKER # handle 20 + oifname "br-dee363d4e0c6" counter packets 0 bytes 0 jump DOCKER # handle 28 + oifname "br-5a9988b94efd" counter packets 0 bytes 0 jump DOCKER # handle 75 + oifname "br-e02b7c89a2d3" counter packets 0 bytes 0 jump DOCKER # handle 139 + oifname "br-875fced92cd0" counter packets 0 bytes 0 jump DOCKER # handle 155 + oifname "docker0" counter packets 0 bytes 0 jump DOCKER # handle 174 + oifname "br-9aea00f5a485" counter packets 0 bytes 0 jump DOCKER # handle 270 + oifname "br-68e2ed0ec6ac" counter packets 0 bytes 0 jump DOCKER # handle 275 + oifname "br-b7976abb6e55" counter packets 556 bytes 33120 jump DOCKER # handle 279 + oifname "br-cfae917b997e" counter packets 0 bytes 0 jump DOCKER # handle 286 + oifname "br-f63e542ab28c" counter packets 0 bytes 0 jump DOCKER # handle 298 + oifname "br-8b881e367322" counter packets 0 bytes 0 jump DOCKER # handle 303 + oifname "br-6caddd639b0b" counter packets 0 bytes 0 jump DOCKER # handle 309 + oifname "br-2d50ca7a8f5d" counter packets 0 bytes 0 jump DOCKER # handle 314 + oifname "br-dc41f9983a47" counter packets 0 bytes 0 jump DOCKER # handle 320 + oifname "br-eeee48934935" counter packets 0 bytes 0 jump DOCKER # handle 325 + oifname "br-f10b040acc46" counter packets 0 bytes 0 jump DOCKER # handle 330 + oifname "br-87e08370adff" counter packets 0 bytes 0 jump DOCKER # handle 336 + oifname "br-ed1bb1cc9d39" counter packets 0 bytes 0 jump DOCKER # handle 344 + oifname "br-527a7cd84ff6" counter packets 1 bytes 52 jump DOCKER # handle 349 + oifname "br-03ee8c9e4bea" counter packets 0 bytes 0 jump DOCKER # handle 356 + oifname "br-f3f5c7cfa74e" counter packets 0 bytes 0 jump DOCKER # handle 362 + oifname "br-263c8508de6f" counter packets 0 bytes 0 jump DOCKER # handle 371 + oifname "br-fea0382ed10b" counter packets 0 bytes 0 jump DOCKER # handle 376 + oifname "br-f9343d8383ac" counter packets 0 bytes 0 jump DOCKER # handle 385 + oifname "br-59ca07fbf200" counter packets 0 bytes 0 jump DOCKER # handle 390 + oifname "br-051bbcfc21d8" counter packets 0 bytes 0 jump DOCKER # handle 395 + oifname "br-4efe811b8402" counter packets 0 bytes 0 jump DOCKER # handle 401 + oifname "br-bca9ecea74f1" counter packets 0 bytes 0 jump DOCKER # handle 406 + oifname "br-a35cbfd37253" counter packets 0 bytes 0 jump DOCKER # handle 412 + oifname "br-71bcae37092b" counter packets 0 bytes 0 jump DOCKER # handle 417 + oifname "br-c50ccbc5e464" counter packets 0 bytes 0 jump DOCKER # handle 422 + oifname "br-618855db6757" counter packets 0 bytes 0 jump DOCKER # handle 439 + oifname "br-28584d053169" counter packets 0 bytes 0 jump DOCKER # handle 445 + oifname "br-4ecbee63f4b0" counter packets 0 bytes 0 jump DOCKER # handle 450 + oifname "br-8200b83f03e9" counter packets 0 bytes 0 jump DOCKER # handle 457 + oifname "br-9891a93b5e9c" counter packets 0 bytes 0 jump DOCKER # handle 462 + } + + chain DOCKER-CT { # handle 7 + oifname "br-11ceaf8736f5" xt match "conntrack" counter packets 5913 bytes 14315682 accept # handle 19 + oifname "br-dee363d4e0c6" xt match "conntrack" counter packets 0 bytes 0 accept # handle 27 + oifname "br-5a9988b94efd" xt match "conntrack" counter packets 260 bytes 99871 accept # handle 74 + oifname "br-e02b7c89a2d3" xt match "conntrack" counter packets 45927 bytes 21960391 accept # handle 138 + oifname "br-875fced92cd0" xt match "conntrack" counter packets 0 bytes 0 accept # handle 154 + oifname "docker0" xt match "conntrack" counter packets 0 bytes 0 accept # handle 173 + oifname "br-9aea00f5a485" xt match "conntrack" counter packets 0 bytes 0 accept # handle 269 + oifname "br-68e2ed0ec6ac" xt match "conntrack" counter packets 451 bytes 117758 accept # handle 274 + oifname "br-b7976abb6e55" xt match "conntrack" counter packets 11952 bytes 824461 accept # handle 278 + oifname "br-cfae917b997e" xt match "conntrack" counter packets 41 bytes 3861 accept # handle 285 + oifname "br-f63e542ab28c" xt match "conntrack" counter packets 0 bytes 0 accept # handle 297 + oifname "br-8b881e367322" xt match "conntrack" counter packets 168 bytes 419234 accept # handle 302 + oifname "br-6caddd639b0b" xt match "conntrack" counter packets 0 bytes 0 accept # handle 308 + oifname "br-2d50ca7a8f5d" xt match "conntrack" counter packets 0 bytes 0 accept # handle 313 + oifname "br-dc41f9983a47" xt match "conntrack" counter packets 0 bytes 0 accept # handle 319 + oifname "br-eeee48934935" xt match "conntrack" counter packets 61 bytes 204337 accept # handle 324 + oifname "br-f10b040acc46" xt match "conntrack" counter packets 2177 bytes 4824454 accept # handle 329 + oifname "br-87e08370adff" xt match "conntrack" counter packets 0 bytes 0 accept # handle 335 + oifname "br-ed1bb1cc9d39" xt match "conntrack" counter packets 15 bytes 11675 accept # handle 343 + oifname "br-527a7cd84ff6" xt match "conntrack" counter packets 4 bytes 207 accept # handle 348 + oifname "br-03ee8c9e4bea" xt match "conntrack" counter packets 7597 bytes 18374987 accept # handle 355 + oifname "br-f3f5c7cfa74e" xt match "conntrack" counter packets 664 bytes 703654 accept # handle 361 + oifname "br-263c8508de6f" xt match "conntrack" counter packets 18991 bytes 2926748 accept # handle 370 + oifname "br-fea0382ed10b" xt match "conntrack" counter packets 103 bytes 71274 accept # handle 375 + oifname "br-f9343d8383ac" xt match "conntrack" counter packets 0 bytes 0 accept # handle 384 + oifname "br-59ca07fbf200" xt match "conntrack" counter packets 0 bytes 0 accept # handle 389 + oifname "br-051bbcfc21d8" xt match "conntrack" counter packets 0 bytes 0 accept # handle 394 + oifname "br-4efe811b8402" xt match "conntrack" counter packets 0 bytes 0 accept # handle 400 + oifname "br-bca9ecea74f1" xt match "conntrack" counter packets 0 bytes 0 accept # handle 405 + oifname "br-a35cbfd37253" xt match "conntrack" counter packets 0 bytes 0 accept # handle 411 + oifname "br-71bcae37092b" xt match "conntrack" counter packets 0 bytes 0 accept # handle 416 + oifname "br-c50ccbc5e464" xt match "conntrack" counter packets 424336 bytes 101769455 accept # handle 421 + oifname "br-618855db6757" xt match "conntrack" counter packets 0 bytes 0 accept # handle 438 + oifname "br-28584d053169" xt match "conntrack" counter packets 12 bytes 6743 accept # handle 444 + oifname "br-4ecbee63f4b0" xt match "conntrack" counter packets 0 bytes 0 accept # handle 449 + oifname "br-8200b83f03e9" xt match "conntrack" counter packets 66 bytes 182937 accept # handle 456 + oifname "br-9891a93b5e9c" xt match "conntrack" counter packets 7240 bytes 40405768 accept # handle 461 + } + + chain DOCKER-INTERNAL { # handle 8 + ip saddr != 172.21.0.0/16 oifname "br-d89dcf384c2a" counter packets 0 bytes 0 drop # handle 288 + ip daddr != 172.21.0.0/16 iifname "br-d89dcf384c2a" counter packets 0 bytes 0 drop # handle 287 + ip saddr != 172.19.0.0/16 oifname "br-ecba5adea6ea" counter packets 0 bytes 0 drop # handle 281 + ip daddr != 172.19.0.0/16 iifname "br-ecba5adea6ea" counter packets 0 bytes 0 drop # handle 280 + } + + chain DOCKER-USER { # handle 169 + } +} +# Warning: table ip6 filter is managed by iptables-nft, do not touch! +table ip6 filter { # handle 2 + chain INPUT { # handle 1 + type filter hook input priority filter; policy accept; + } + + chain FORWARD { # handle 2 + type filter hook forward priority filter; policy drop; + counter packets 1704 bytes 966686 jump DOCKER-USER # handle 86 + counter packets 1704 bytes 966686 jump DOCKER-FORWARD # handle 9 + } + + chain OUTPUT { # handle 3 + type filter hook output priority filter; policy accept; + } + + chain DOCKER { # handle 4 + ip6 daddr fd00:9::2 iifname != "br-8200b83f03e9" oifname "br-8200b83f03e9" tcp dport 8096 counter packets 0 bytes 0 accept # handle 177 + ip6 daddr fd00:17::2 iifname != "br-28584d053169" oifname "br-28584d053169" tcp dport 9980 counter packets 0 bytes 0 accept # handle 167 + ip6 daddr fd00:1::6 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 4533 counter packets 0 bytes 0 accept # handle 162 + ip6 daddr fd00:10::2 iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" tcp dport 443 counter packets 0 bytes 0 accept # handle 156 + ip6 daddr fd00:10::2 iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" tcp dport 80 counter packets 0 bytes 0 accept # handle 155 + ip6 daddr fd00:1::5 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 8080 counter packets 0 bytes 0 accept # handle 150 + ip6 daddr fd00:1::3 iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" tcp dport 80 counter packets 0 bytes 0 accept # handle 144 + ip6 daddr fd00:24::2 iifname != "br-f9343d8383ac" oifname "br-f9343d8383ac" tcp dport 8080 counter packets 0 bytes 0 accept # handle 143 + ip6 daddr fd00:30::4 iifname != "br-dc41f9983a47" oifname "br-dc41f9983a47" tcp dport 80 counter packets 0 bytes 0 accept # handle 114 + iifname != "br-5a9988b94efd" oifname "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 34 + iifname != "br-9aea00f5a485" oifname "br-9aea00f5a485" counter packets 0 bytes 0 drop # handle 99 + iifname != "br-f63e542ab28c" oifname "br-f63e542ab28c" counter packets 0 bytes 0 drop # handle 103 + iifname != "br-8b881e367322" oifname "br-8b881e367322" counter packets 0 bytes 0 drop # handle 107 + iifname != "br-dc41f9983a47" oifname "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 111 + iifname != "br-f10b040acc46" oifname "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 116 + iifname != "br-87e08370adff" oifname "br-87e08370adff" counter packets 0 bytes 0 drop # handle 120 + iifname != "br-ed1bb1cc9d39" oifname "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 124 + iifname != "br-527a7cd84ff6" oifname "br-527a7cd84ff6" counter packets 0 bytes 0 drop # handle 128 + iifname != "br-f3f5c7cfa74e" oifname "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 132 + iifname != "br-263c8508de6f" oifname "br-263c8508de6f" counter packets 0 bytes 0 drop # handle 136 + iifname != "br-f9343d8383ac" oifname "br-f9343d8383ac" counter packets 0 bytes 0 drop # handle 140 + iifname != "br-051bbcfc21d8" oifname "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 146 + iifname != "br-bca9ecea74f1" oifname "br-bca9ecea74f1" counter packets 0 bytes 0 drop # handle 152 + iifname != "br-618855db6757" oifname "br-618855db6757" counter packets 0 bytes 0 drop # handle 158 + iifname != "br-28584d053169" oifname "br-28584d053169" counter packets 0 bytes 0 drop # handle 164 + iifname != "br-4ecbee63f4b0" oifname "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 169 + iifname != "br-8200b83f03e9" oifname "br-8200b83f03e9" counter packets 0 bytes 0 drop # handle 174 + } + + chain DOCKER-FORWARD { # handle 5 + counter packets 1704 bytes 966686 jump DOCKER-CT # handle 12 + counter packets 798 bytes 112527 jump DOCKER-INTERNAL # handle 11 + counter packets 798 bytes 112527 jump DOCKER-BRIDGE # handle 10 + iifname "br-5a9988b94efd" counter packets 27 bytes 5590 accept # handle 33 + iifname "br-9aea00f5a485" counter packets 0 bytes 0 accept # handle 98 + iifname "br-f63e542ab28c" counter packets 0 bytes 0 accept # handle 102 + iifname "br-8b881e367322" counter packets 0 bytes 0 accept # handle 106 + iifname "br-dc41f9983a47" counter packets 0 bytes 0 accept # handle 110 + iifname "br-f10b040acc46" counter packets 0 bytes 0 accept # handle 115 + iifname "br-87e08370adff" counter packets 0 bytes 0 accept # handle 119 + iifname "br-ed1bb1cc9d39" counter packets 0 bytes 0 accept # handle 123 + iifname "br-527a7cd84ff6" counter packets 0 bytes 0 accept # handle 127 + iifname "br-f3f5c7cfa74e" counter packets 544 bytes 83353 accept # handle 131 + iifname "br-263c8508de6f" counter packets 44 bytes 5795 accept # handle 135 + iifname "br-f9343d8383ac" counter packets 0 bytes 0 accept # handle 139 + iifname "br-051bbcfc21d8" counter packets 0 bytes 0 accept # handle 145 + iifname "br-bca9ecea74f1" counter packets 122 bytes 10531 accept # handle 151 + iifname "br-618855db6757" counter packets 0 bytes 0 accept # handle 157 + iifname "br-28584d053169" counter packets 0 bytes 0 accept # handle 163 + iifname "br-4ecbee63f4b0" counter packets 0 bytes 0 accept # handle 168 + iifname "br-8200b83f03e9" counter packets 0 bytes 0 accept # handle 173 + } + + chain DOCKER-BRIDGE { # handle 6 + oifname "br-5a9988b94efd" counter packets 0 bytes 0 jump DOCKER # handle 36 + oifname "br-9aea00f5a485" counter packets 0 bytes 0 jump DOCKER # handle 101 + oifname "br-f63e542ab28c" counter packets 0 bytes 0 jump DOCKER # handle 105 + oifname "br-8b881e367322" counter packets 0 bytes 0 jump DOCKER # handle 109 + oifname "br-dc41f9983a47" counter packets 0 bytes 0 jump DOCKER # handle 113 + oifname "br-f10b040acc46" counter packets 0 bytes 0 jump DOCKER # handle 118 + oifname "br-87e08370adff" counter packets 0 bytes 0 jump DOCKER # handle 122 + oifname "br-ed1bb1cc9d39" counter packets 0 bytes 0 jump DOCKER # handle 126 + oifname "br-527a7cd84ff6" counter packets 0 bytes 0 jump DOCKER # handle 130 + oifname "br-f3f5c7cfa74e" counter packets 0 bytes 0 jump DOCKER # handle 134 + oifname "br-263c8508de6f" counter packets 0 bytes 0 jump DOCKER # handle 138 + oifname "br-f9343d8383ac" counter packets 0 bytes 0 jump DOCKER # handle 142 + oifname "br-051bbcfc21d8" counter packets 0 bytes 0 jump DOCKER # handle 148 + oifname "br-bca9ecea74f1" counter packets 0 bytes 0 jump DOCKER # handle 154 + oifname "br-618855db6757" counter packets 0 bytes 0 jump DOCKER # handle 160 + oifname "br-28584d053169" counter packets 0 bytes 0 jump DOCKER # handle 166 + oifname "br-4ecbee63f4b0" counter packets 0 bytes 0 jump DOCKER # handle 171 + oifname "br-8200b83f03e9" counter packets 0 bytes 0 jump DOCKER # handle 176 + } + + chain DOCKER-CT { # handle 7 + oifname "br-5a9988b94efd" xt match "conntrack" counter packets 28 bytes 16684 accept # handle 35 + oifname "br-9aea00f5a485" xt match "conntrack" counter packets 0 bytes 0 accept # handle 100 + oifname "br-f63e542ab28c" xt match "conntrack" counter packets 0 bytes 0 accept # handle 104 + oifname "br-8b881e367322" xt match "conntrack" counter packets 0 bytes 0 accept # handle 108 + oifname "br-dc41f9983a47" xt match "conntrack" counter packets 0 bytes 0 accept # handle 112 + oifname "br-f10b040acc46" xt match "conntrack" counter packets 0 bytes 0 accept # handle 117 + oifname "br-87e08370adff" xt match "conntrack" counter packets 0 bytes 0 accept # handle 121 + oifname "br-ed1bb1cc9d39" xt match "conntrack" counter packets 0 bytes 0 accept # handle 125 + oifname "br-527a7cd84ff6" xt match "conntrack" counter packets 0 bytes 0 accept # handle 129 + oifname "br-f3f5c7cfa74e" xt match "conntrack" counter packets 576 bytes 474421 accept # handle 133 + oifname "br-263c8508de6f" xt match "conntrack" counter packets 38 bytes 15719 accept # handle 137 + oifname "br-f9343d8383ac" xt match "conntrack" counter packets 0 bytes 0 accept # handle 141 + oifname "br-051bbcfc21d8" xt match "conntrack" counter packets 0 bytes 0 accept # handle 147 + oifname "br-bca9ecea74f1" xt match "conntrack" counter packets 225 bytes 333045 accept # handle 153 + oifname "br-618855db6757" xt match "conntrack" counter packets 0 bytes 0 accept # handle 159 + oifname "br-28584d053169" xt match "conntrack" counter packets 0 bytes 0 accept # handle 165 + oifname "br-4ecbee63f4b0" xt match "conntrack" counter packets 0 bytes 0 accept # handle 170 + oifname "br-8200b83f03e9" xt match "conntrack" counter packets 0 bytes 0 accept # handle 175 + } + + chain DOCKER-INTERNAL { # handle 8 + } + + chain DOCKER-USER { # handle 85 + } +} +# Warning: table ip nat is managed by iptables-nft, do not touch! +table ip nat { # handle 3 + chain DOCKER { # handle 1 + ip daddr 127.0.0.1 iifname != "docker0" tcp dport 3307 counter packets 0 bytes 0 xt target "DNAT" # handle 49 + ip daddr 127.0.0.1 iifname != "docker0" tcp dport 5434 counter packets 0 bytes 0 xt target "DNAT" # handle 55 + ip daddr 127.0.0.1 iifname != "br-11ceaf8736f5" tcp dport 3005 counter packets 0 bytes 0 xt target "DNAT" # handle 72 + ip daddr 127.0.0.1 iifname != "br-11ceaf8736f5" tcp dport 7001 counter packets 0 bytes 0 xt target "DNAT" # handle 80 + ip daddr 127.0.0.1 iifname != "br-9aea00f5a485" tcp dport 8440 counter packets 0 bytes 0 xt target "DNAT" # handle 141 + ip daddr 10.98.0.2 iifname != "br-b7976abb6e55" tcp dport 25 counter packets 500 bytes 29920 xt target "DNAT" # handle 145 + ip daddr 127.0.0.1 iifname != "br-b7976abb6e55" tcp dport 8088 counter packets 0 bytes 0 xt target "DNAT" # handle 146 + ip daddr 127.0.0.1 iifname != "br-b7976abb6e55" tcp dport 8443 counter packets 0 bytes 0 xt target "DNAT" # handle 147 + ip daddr 10.98.0.2 iifname != "br-b7976abb6e55" tcp dport 465 counter packets 23 bytes 1252 xt target "DNAT" # handle 148 + ip daddr 10.98.0.2 iifname != "br-b7976abb6e55" tcp dport 993 counter packets 33 bytes 1948 xt target "DNAT" # handle 149 + ip daddr 127.0.0.1 iifname != "br-f63e542ab28c" tcp dport 8017 counter packets 0 bytes 0 xt target "DNAT" # handle 151 + ip daddr 127.0.0.1 iifname != "br-6caddd639b0b" tcp dport 8765 counter packets 0 bytes 0 xt target "DNAT" # handle 156 + ip daddr 127.0.0.1 iifname != "br-2d50ca7a8f5d" tcp dport 1935 counter packets 0 bytes 0 xt target "DNAT" # handle 158 + ip daddr 127.0.0.1 iifname != "br-2d50ca7a8f5d" tcp dport 8086 counter packets 0 bytes 0 xt target "DNAT" # handle 159 + iifname != "br-dc41f9983a47" tcp dport 9098 counter packets 0 bytes 0 xt target "DNAT" # handle 161 + ip daddr 127.0.0.1 iifname != "br-eeee48934935" tcp dport 7576 counter packets 0 bytes 0 xt target "DNAT" # handle 163 + ip daddr 127.0.0.1 iifname != "br-f10b040acc46" tcp dport 3000 counter packets 0 bytes 0 xt target "DNAT" # handle 166 + ip daddr 10.98.0.2 iifname != "br-87e08370adff" tcp dport 2244 counter packets 0 bytes 0 xt target "DNAT" # handle 171 + ip daddr 127.0.0.1 iifname != "br-87e08370adff" tcp dport 2244 counter packets 0 bytes 0 xt target "DNAT" # handle 172 + ip daddr 127.0.0.1 iifname != "br-87e08370adff" tcp dport 3006 counter packets 0 bytes 0 xt target "DNAT" # handle 173 + ip daddr 127.0.0.1 iifname != "br-ed1bb1cc9d39" tcp dport 8181 counter packets 0 bytes 0 xt target "DNAT" # handle 175 + ip daddr 10.98.0.2 iifname != "br-527a7cd84ff6" tcp dport 6667 counter packets 0 bytes 0 xt target "DNAT" # handle 177 + ip daddr 10.98.0.2 iifname != "br-527a7cd84ff6" tcp dport 6697 counter packets 1 bytes 52 xt target "DNAT" # handle 178 + ip daddr 127.0.0.1 iifname != "br-527a7cd84ff6" tcp dport 8097 counter packets 0 bytes 0 xt target "DNAT" # handle 179 + ip daddr 127.0.0.1 iifname != "br-03ee8c9e4bea" tcp dport 1236 counter packets 0 bytes 0 xt target "DNAT" # handle 181 + ip daddr 127.0.0.1 iifname != "br-03ee8c9e4bea" tcp dport 8536 counter packets 0 bytes 0 xt target "DNAT" # handle 182 + ip daddr 127.0.0.1 iifname != "br-f3f5c7cfa74e" tcp dport 9201 counter packets 0 bytes 0 xt target "DNAT" # handle 184 + ip daddr 127.0.0.1 iifname != "br-f3f5c7cfa74e" tcp dport 3001 counter packets 0 bytes 0 xt target "DNAT" # handle 187 + ip daddr 127.0.0.1 iifname != "br-f3f5c7cfa74e" tcp dport 4001 counter packets 0 bytes 0 xt target "DNAT" # handle 188 + ip daddr 127.0.0.1 iifname != "br-263c8508de6f" tcp dport 6167 counter packets 0 bytes 0 xt target "DNAT" # handle 190 + ip daddr 127.0.0.1 iifname != "br-fea0382ed10b" tcp dport 5280 counter packets 0 bytes 0 xt target "DNAT" # handle 192 + ip daddr 127.0.0.1 iifname != "br-fea0382ed10b" tcp dport 8002 counter packets 0 bytes 0 xt target "DNAT" # handle 193 + ip daddr 127.0.0.1 iifname != "br-fea0382ed10b" tcp dport 9090 counter packets 0 bytes 0 xt target "DNAT" # handle 194 + iifname != "br-fea0382ed10b" udp dport 10009 counter packets 0 bytes 0 xt target "DNAT" # handle 195 + ip daddr 127.0.0.1 iifname != "br-fea0382ed10b" tcp dport 8001 counter packets 0 bytes 0 xt target "DNAT" # handle 196 + iifname != "br-f9343d8383ac" tcp dport 8383 counter packets 0 bytes 0 xt target "DNAT" # handle 198 + iifname != "br-5a9988b94efd" tcp dport 7778 counter packets 0 bytes 0 xt target "DNAT" # handle 200 + iifname != "br-5a9988b94efd" tcp dport 8016 counter packets 0 bytes 0 xt target "DNAT" # handle 203 + iifname != "br-e02b7c89a2d3" tcp dport 9009 counter packets 0 bytes 0 xt target "DNAT" # handle 205 + iifname != "br-bca9ecea74f1" tcp dport 8090 counter packets 0 bytes 0 xt target "DNAT" # handle 207 + iifname != "br-bca9ecea74f1" tcp dport 444 counter packets 0 bytes 0 xt target "DNAT" # handle 208 + iifname != "br-a35cbfd37253" tcp dport 8000 counter packets 0 bytes 0 xt target "DNAT" # handle 210 + ip daddr 127.0.0.1 iifname != "br-71bcae37092b" tcp dport 7676 counter packets 0 bytes 0 xt target "DNAT" # handle 212 + iifname != "br-c50ccbc5e464" tcp dport 5000 counter packets 0 bytes 0 xt target "DNAT" # handle 214 + iifname != "br-c50ccbc5e464" tcp dport 5055 counter packets 0 bytes 0 xt target "DNAT" # handle 215 + iifname != "br-c50ccbc5e464" tcp dport 5075 counter packets 0 bytes 0 xt target "DNAT" # handle 216 + iifname != "br-c50ccbc5e464" tcp dport 6767 counter packets 0 bytes 0 xt target "DNAT" # handle 217 + iifname != "br-c50ccbc5e464" tcp dport 7878 counter packets 0 bytes 0 xt target "DNAT" # handle 218 + iifname != "br-c50ccbc5e464" tcp dport 8080 counter packets 0 bytes 0 xt target "DNAT" # handle 219 + iifname != "br-c50ccbc5e464" tcp dport 8191 counter packets 0 bytes 0 xt target "DNAT" # handle 220 + iifname != "br-c50ccbc5e464" tcp dport 8265 counter packets 0 bytes 0 xt target "DNAT" # handle 221 + iifname != "br-c50ccbc5e464" tcp dport 8787 counter packets 0 bytes 0 xt target "DNAT" # handle 222 + iifname != "br-c50ccbc5e464" tcp dport 8888 counter packets 0 bytes 0 xt target "DNAT" # handle 223 + iifname != "br-c50ccbc5e464" tcp dport 8989 counter packets 0 bytes 0 xt target "DNAT" # handle 224 + iifname != "br-c50ccbc5e464" tcp dport 9696 counter packets 0 bytes 0 xt target "DNAT" # handle 225 + iifname != "br-c50ccbc5e464" tcp dport 5030 counter packets 0 bytes 0 xt target "DNAT" # handle 226 + iifname != "br-5a9988b94efd" tcp dport 4533 counter packets 0 bytes 0 xt target "DNAT" # handle 229 + iifname != "br-28584d053169" tcp dport 9980 counter packets 0 bytes 0 xt target "DNAT" # handle 231 + iifname != "br-8200b83f03e9" tcp dport 8096 counter packets 0 bytes 0 xt target "DNAT" # handle 237 + ip daddr 127.0.0.1 iifname != "br-9891a93b5e9c" tcp dport 8945 counter packets 0 bytes 0 xt target "DNAT" # handle 239 + } + + chain PREROUTING { # handle 2 + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 12868 bytes 773053 jump DOCKER # handle 3 + } + + chain OUTPUT { # handle 4 + type nat hook output priority dstnat; policy accept; + ip daddr != 127.0.0.0/8 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER # handle 5 + } + + chain POSTROUTING { # handle 6 + type nat hook postrouting priority srcnat; policy accept; + ip saddr 172.31.0.0/16 oifname != "br-9891a93b5e9c" counter packets 3 bytes 180 xt target "MASQUERADE" # handle 238 + ip saddr 10.9.0.0/16 oifname != "br-8200b83f03e9" counter packets 2 bytes 120 xt target "MASQUERADE" # handle 236 + ip saddr 10.18.0.0/16 oifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 232 + ip saddr 10.17.0.0/16 oifname != "br-28584d053169" counter packets 1 bytes 60 xt target "MASQUERADE" # handle 230 + ip saddr 10.16.0.0/16 oifname != "br-618855db6757" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 227 + ip saddr 10.19.0.0/16 oifname != "br-c50ccbc5e464" counter packets 4 bytes 378 xt target "MASQUERADE" # handle 213 + ip saddr 172.30.0.0/16 oifname != "br-71bcae37092b" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 211 + ip saddr 10.13.0.0/16 oifname != "br-a35cbfd37253" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 209 + ip saddr 10.10.0.0/16 oifname != "br-bca9ecea74f1" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 206 + ip saddr 172.27.0.0/16 oifname != "br-4efe811b8402" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 204 + ip saddr 10.41.0.0/16 oifname != "br-051bbcfc21d8" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 201 + ip saddr 10.32.0.0/16 oifname != "br-59ca07fbf200" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 199 + ip saddr 10.24.0.0/16 oifname != "br-f9343d8383ac" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 197 + ip saddr 172.26.0.0/16 oifname != "br-fea0382ed10b" counter packets 12 bytes 3156 xt target "MASQUERADE" # handle 191 + ip saddr 10.26.0.0/16 oifname != "br-263c8508de6f" counter packets 17780 bytes 1459511 xt target "MASQUERADE" # handle 189 + ip saddr 10.31.0.0/16 oifname != "br-f3f5c7cfa74e" counter packets 87 bytes 5220 xt target "MASQUERADE" # handle 183 + ip saddr 172.28.0.0/16 oifname != "br-03ee8c9e4bea" counter packets 43 bytes 2580 xt target "MASQUERADE" # handle 180 + ip saddr 10.20.0.0/16 oifname != "br-527a7cd84ff6" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 176 + ip saddr 10.28.0.0/16 oifname != "br-ed1bb1cc9d39" counter packets 1 bytes 60 xt target "MASQUERADE" # handle 174 + ip saddr 10.27.0.0/16 oifname != "br-87e08370adff" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 167 + ip saddr 10.29.0.0/16 oifname != "br-f10b040acc46" counter packets 28 bytes 1680 xt target "MASQUERADE" # handle 164 + ip saddr 172.24.0.0/16 oifname != "br-eeee48934935" counter packets 1 bytes 60 xt target "MASQUERADE" # handle 162 + ip saddr 10.30.0.0/16 oifname != "br-dc41f9983a47" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 160 + ip saddr 172.23.0.0/16 oifname != "br-2d50ca7a8f5d" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 157 + ip saddr 172.22.0.0/16 oifname != "br-6caddd639b0b" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 155 + ip saddr 10.40.0.0/16 oifname != "br-8b881e367322" counter packets 3 bytes 180 xt target "MASQUERADE" # handle 152 + ip saddr 10.4.0.0/16 oifname != "br-f63e542ab28c" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 150 + ip saddr 172.20.0.0/16 oifname != "br-cfae917b997e" counter packets 9 bytes 1092 xt target "MASQUERADE" # handle 144 + ip saddr 172.18.0.0/16 oifname != "br-b7976abb6e55" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 143 + ip saddr 10.2.0.0/16 oifname != "br-68e2ed0ec6ac" counter packets 492 bytes 40183 xt target "MASQUERADE" # handle 142 + ip saddr 10.3.0.0/16 oifname != "br-9aea00f5a485" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 140 + ip saddr 172.17.0.0/16 oifname != "docker0" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 46 + ip saddr 172.25.0.0/16 oifname != "br-875fced92cd0" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 41 + ip saddr 10.99.0.0/16 oifname != "br-e02b7c89a2d3" counter packets 4000 bytes 239992 xt target "MASQUERADE" # handle 37 + ip saddr 10.1.0.0/16 oifname != "br-5a9988b94efd" counter packets 7 bytes 420 xt target "MASQUERADE" # handle 21 + ip saddr 172.29.0.0/16 oifname != "br-dee363d4e0c6" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 10 + ip saddr 10.55.0.0/16 oifname != "br-11ceaf8736f5" counter packets 10 bytes 600 xt target "MASQUERADE" # handle 8 + } +} +# Warning: table ip6 nat is managed by iptables-nft, do not touch! +table ip6 nat { # handle 4 + chain DOCKER { # handle 1 + ip6 saddr != fe80::/10 iifname != "br-dc41f9983a47" tcp dport 9098 counter packets 0 bytes 0 xt target "DNAT" # handle 40 + ip6 saddr != fe80::/10 iifname != "br-f9343d8383ac" tcp dport 8383 counter packets 0 bytes 0 xt target "DNAT" # handle 48 + ip6 saddr != fe80::/10 iifname != "br-5a9988b94efd" tcp dport 7778 counter packets 0 bytes 0 xt target "DNAT" # handle 49 + ip6 saddr != fe80::/10 iifname != "br-5a9988b94efd" tcp dport 8016 counter packets 0 bytes 0 xt target "DNAT" # handle 52 + ip6 saddr != fe80::/10 iifname != "br-bca9ecea74f1" tcp dport 8090 counter packets 0 bytes 0 xt target "DNAT" # handle 54 + ip6 saddr != fe80::/10 iifname != "br-bca9ecea74f1" tcp dport 444 counter packets 0 bytes 0 xt target "DNAT" # handle 55 + ip6 saddr != fe80::/10 iifname != "br-5a9988b94efd" tcp dport 4533 counter packets 0 bytes 0 xt target "DNAT" # handle 58 + ip6 saddr != fe80::/10 iifname != "br-28584d053169" tcp dport 9980 counter packets 0 bytes 0 xt target "DNAT" # handle 60 + ip6 saddr != fe80::/10 iifname != "br-8200b83f03e9" tcp dport 8096 counter packets 0 bytes 0 xt target "DNAT" # handle 64 + } + + chain PREROUTING { # handle 2 + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 2 bytes 112 jump DOCKER # handle 3 + } + + chain OUTPUT { # handle 4 + type nat hook output priority dstnat; policy accept; + ip6 daddr != ::1 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER # handle 5 + } + + chain POSTROUTING { # handle 6 + type nat hook postrouting priority srcnat; policy accept; + ip6 saddr fd00:9::/64 oifname != "br-8200b83f03e9" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 63 + ip6 saddr fd00:18::/64 oifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 61 + ip6 saddr fd00:17::/64 oifname != "br-28584d053169" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 59 + ip6 saddr fd00:16::/64 oifname != "br-618855db6757" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 56 + ip6 saddr fd00:10::/64 oifname != "br-bca9ecea74f1" counter packets 1 bytes 80 xt target "MASQUERADE" # handle 53 + ip6 saddr fd00:41::/64 oifname != "br-051bbcfc21d8" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 50 + ip6 saddr fd00:24::/64 oifname != "br-f9343d8383ac" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 47 + ip6 saddr fd00:26::/64 oifname != "br-263c8508de6f" counter packets 5 bytes 400 xt target "MASQUERADE" # handle 46 + ip6 saddr fd00:31::/64 oifname != "br-f3f5c7cfa74e" counter packets 60 bytes 4800 xt target "MASQUERADE" # handle 45 + ip6 saddr fd00:20::/64 oifname != "br-527a7cd84ff6" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 44 + ip6 saddr fd00:28::/64 oifname != "br-ed1bb1cc9d39" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 43 + ip6 saddr fd00:27::/64 oifname != "br-87e08370adff" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 42 + ip6 saddr fd00:29::/64 oifname != "br-f10b040acc46" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 41 + ip6 saddr fd00:30::/64 oifname != "br-dc41f9983a47" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 39 + ip6 saddr fd00:40::/64 oifname != "br-8b881e367322" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 38 + ip6 saddr fd00:4::/64 oifname != "br-f63e542ab28c" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 37 + ip6 saddr fd00:3::/64 oifname != "br-9aea00f5a485" counter packets 0 bytes 0 xt target "MASQUERADE" # handle 36 + ip6 saddr fd00:1::/64 oifname != "br-5a9988b94efd" counter packets 2 bytes 160 xt target "MASQUERADE" # handle 12 + } +} +table ip raw { # handle 5 + chain PREROUTING { # handle 1 + type filter hook prerouting priority raw; policy accept; + ip daddr 10.1.0.17 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 4 + ip daddr 10.1.0.20 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 7 + ip daddr 10.99.0.18 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 8 + ip daddr 10.1.0.9 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 9 + ip daddr 10.1.0.12 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 18 + ip daddr 10.1.0.19 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 19 + ip daddr 10.1.0.22 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 20 + ip daddr 172.17.0.2 iifname != "docker0" counter packets 0 bytes 0 drop # handle 27 + ip daddr 172.17.0.3 iifname != "docker0" counter packets 0 bytes 0 drop # handle 32 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 3307 counter packets 0 bytes 0 drop # handle 39 + ip daddr 172.17.0.4 iifname != "docker0" counter packets 0 bytes 0 drop # handle 48 + ip daddr 10.55.0.2 iifname != "br-11ceaf8736f5" counter packets 0 bytes 0 drop # handle 53 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 5434 counter packets 0 bytes 0 drop # handle 66 + ip daddr 10.55.0.3 iifname != "br-11ceaf8736f5" counter packets 0 bytes 0 drop # handle 85 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 3005 counter packets 0 bytes 0 drop # handle 104 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 7001 counter packets 0 bytes 0 drop # handle 118 + ip daddr 10.99.0.2 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 251 + ip daddr 10.99.0.3 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 252 + ip daddr 10.3.0.2 iifname != "br-9aea00f5a485" counter packets 0 bytes 0 drop # handle 253 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8440 counter packets 0 bytes 0 drop # handle 254 + ip daddr 172.18.0.2 iifname != "br-b7976abb6e55" counter packets 0 bytes 0 drop # handle 255 + ip daddr 172.20.0.2 iifname != "br-cfae917b997e" counter packets 0 bytes 0 drop # handle 256 + ip daddr 10.2.255.254 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 257 + ip daddr 10.2.0.2 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 258 + ip daddr 172.18.0.3 iifname != "br-b7976abb6e55" counter packets 0 bytes 0 drop # handle 259 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8088 counter packets 0 bytes 0 drop # handle 260 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8443 counter packets 0 bytes 0 drop # handle 261 + ip daddr 10.2.0.3 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 262 + ip daddr 10.2.0.4 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 263 + ip daddr 172.20.0.3 iifname != "br-cfae917b997e" counter packets 0 bytes 0 drop # handle 264 + ip daddr 10.2.0.5 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 265 + ip daddr 10.2.0.6 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 266 + ip daddr 10.2.0.7 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 267 + ip daddr 10.2.0.8 iifname != "br-68e2ed0ec6ac" counter packets 0 bytes 0 drop # handle 268 + ip daddr 10.4.0.2 iifname != "br-f63e542ab28c" counter packets 0 bytes 0 drop # handle 269 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8017 counter packets 0 bytes 0 drop # handle 270 + ip daddr 172.22.0.2 iifname != "br-6caddd639b0b" counter packets 0 bytes 0 drop # handle 275 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8765 counter packets 0 bytes 0 drop # handle 276 + ip daddr 172.23.0.2 iifname != "br-2d50ca7a8f5d" counter packets 0 bytes 0 drop # handle 277 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 1935 counter packets 0 bytes 0 drop # handle 278 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8086 counter packets 0 bytes 0 drop # handle 279 + ip daddr 10.30.0.2 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 280 + ip daddr 10.30.0.3 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 281 + ip daddr 10.1.0.2 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 282 + ip daddr 10.99.0.5 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 283 + ip daddr 10.30.0.4 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 284 + ip daddr 172.24.0.2 iifname != "br-eeee48934935" counter packets 0 bytes 0 drop # handle 285 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 7576 counter packets 0 bytes 0 drop # handle 286 + ip daddr 10.29.0.2 iifname != "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 287 + ip daddr 10.99.0.6 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 288 + ip daddr 10.29.0.3 iifname != "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 290 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 3000 counter packets 0 bytes 0 drop # handle 291 + ip daddr 10.99.0.7 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 292 + ip daddr 10.27.0.2 iifname != "br-87e08370adff" counter packets 0 bytes 0 drop # handle 295 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 2244 counter packets 0 bytes 0 drop # handle 296 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 3006 counter packets 0 bytes 0 drop # handle 297 + ip daddr 10.28.0.2 iifname != "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 298 + ip daddr 10.28.0.3 iifname != "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 299 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8181 counter packets 0 bytes 0 drop # handle 300 + ip daddr 10.99.0.8 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 301 + ip daddr 10.20.0.2 iifname != "br-527a7cd84ff6" counter packets 0 bytes 0 drop # handle 302 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8097 counter packets 0 bytes 0 drop # handle 303 + ip daddr 172.28.0.2 iifname != "br-03ee8c9e4bea" counter packets 0 bytes 0 drop # handle 304 + ip daddr 10.99.0.9 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 305 + ip daddr 172.28.0.3 iifname != "br-03ee8c9e4bea" counter packets 0 bytes 0 drop # handle 306 + ip daddr 172.28.0.4 iifname != "br-03ee8c9e4bea" counter packets 0 bytes 0 drop # handle 307 + ip daddr 172.28.0.5 iifname != "br-03ee8c9e4bea" counter packets 0 bytes 0 drop # handle 308 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 1236 counter packets 0 bytes 0 drop # handle 309 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8536 counter packets 0 bytes 0 drop # handle 310 + ip daddr 10.31.0.2 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 311 + ip daddr 10.31.0.3 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 312 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 9201 counter packets 0 bytes 0 drop # handle 313 + ip daddr 10.99.0.10 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 314 + ip daddr 10.99.0.11 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 315 + ip daddr 10.99.0.12 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 316 + ip daddr 10.31.0.4 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 319 + ip daddr 10.31.0.5 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 320 + ip daddr 10.31.0.6 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 321 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 3001 counter packets 0 bytes 0 drop # handle 322 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 4001 counter packets 0 bytes 0 drop # handle 323 + ip daddr 10.26.0.2 iifname != "br-263c8508de6f" counter packets 0 bytes 0 drop # handle 324 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 6167 counter packets 0 bytes 0 drop # handle 325 + ip daddr 172.26.0.2 iifname != "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 326 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 5280 counter packets 0 bytes 0 drop # handle 327 + ip daddr 172.26.0.3 iifname != "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 328 + ip daddr 172.26.0.4 iifname != "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 329 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8002 counter packets 0 bytes 0 drop # handle 330 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 9090 counter packets 0 bytes 0 drop # handle 331 + ip daddr 172.26.0.5 iifname != "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 332 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8001 counter packets 0 bytes 0 drop # handle 333 + ip daddr 172.26.0.6 iifname != "br-fea0382ed10b" counter packets 0 bytes 0 drop # handle 334 + ip daddr 10.24.0.2 iifname != "br-f9343d8383ac" counter packets 0 bytes 0 drop # handle 335 + ip daddr 10.32.0.2 iifname != "br-59ca07fbf200" counter packets 0 bytes 0 drop # handle 336 + ip daddr 10.1.0.3 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 337 + ip daddr 10.1.0.4 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 338 + ip daddr 10.99.0.13 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 339 + ip daddr 10.99.0.14 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 340 + ip daddr 10.32.0.3 iifname != "br-59ca07fbf200" counter packets 0 bytes 0 drop # handle 341 + ip daddr 10.32.0.4 iifname != "br-59ca07fbf200" counter packets 0 bytes 0 drop # handle 342 + ip daddr 10.41.0.2 iifname != "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 343 + ip daddr 10.41.0.3 iifname != "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 344 + ip daddr 10.1.0.5 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 345 + ip daddr 10.99.0.15 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 346 + ip daddr 172.27.0.2 iifname != "br-4efe811b8402" counter packets 0 bytes 0 drop # handle 347 + ip daddr 10.99.0.16 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 348 + ip daddr 172.27.0.3 iifname != "br-4efe811b8402" counter packets 0 bytes 0 drop # handle 349 + ip daddr 10.10.0.2 iifname != "br-bca9ecea74f1" counter packets 0 bytes 0 drop # handle 350 + ip daddr 10.13.0.2 iifname != "br-a35cbfd37253" counter packets 0 bytes 0 drop # handle 351 + ip daddr 172.30.0.2 iifname != "br-71bcae37092b" counter packets 0 bytes 0 drop # handle 352 + ip daddr 172.30.0.3 iifname != "br-71bcae37092b" counter packets 0 bytes 0 drop # handle 355 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 7676 counter packets 0 bytes 0 drop # handle 356 + ip daddr 10.19.0.2 iifname != "br-c50ccbc5e464" counter packets 0 bytes 0 drop # handle 357 + ip daddr 10.19.0.3 iifname != "br-c50ccbc5e464" counter packets 0 bytes 0 drop # handle 358 + ip daddr 10.19.0.4 iifname != "br-c50ccbc5e464" counter packets 0 bytes 0 drop # handle 359 + ip daddr 10.16.0.2 iifname != "br-618855db6757" counter packets 0 bytes 0 drop # handle 360 + ip daddr 10.1.0.6 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 361 + ip daddr 10.99.0.17 iifname != "br-e02b7c89a2d3" counter packets 0 bytes 0 drop # handle 362 + ip daddr 10.17.0.2 iifname != "br-28584d053169" counter packets 0 bytes 0 drop # handle 363 + ip daddr 10.18.0.2 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 366 + ip daddr 10.18.0.3 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 367 + ip daddr 10.18.0.4 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 368 + ip daddr 10.9.0.2 iifname != "br-8200b83f03e9" counter packets 0 bytes 0 drop # handle 370 + ip daddr 172.31.0.2 iifname != "br-9891a93b5e9c" counter packets 0 bytes 0 drop # handle 371 + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8945 counter packets 0 bytes 0 drop # handle 372 + } +} +table ip6 raw { # handle 6 + chain PREROUTING { # handle 1 + type filter hook prerouting priority raw; policy accept; + ip6 daddr fd00:1::11 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 3 + ip6 daddr fd00:1::14 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 4 + ip6 daddr fd00:1::9 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 5 + ip6 daddr fd00:1::c iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 7 + ip6 daddr fd00:1::13 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 8 + ip6 daddr fd00:1::16 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 9 + ip6 daddr fd00:3::2 iifname != "br-9aea00f5a485" counter packets 0 bytes 0 drop # handle 38 + ip6 daddr fd00:4::2 iifname != "br-f63e542ab28c" counter packets 0 bytes 0 drop # handle 39 + ip6 daddr fd00:30::2 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 41 + ip6 daddr fd00:30::3 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 42 + ip6 daddr fd00:1::2 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 43 + ip6 daddr fd00:30::4 iifname != "br-dc41f9983a47" counter packets 0 bytes 0 drop # handle 44 + ip6 daddr fd00:29::2 iifname != "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 45 + ip6 daddr fd00:29::3 iifname != "br-f10b040acc46" counter packets 0 bytes 0 drop # handle 46 + ip6 daddr fd00:27::2 iifname != "br-87e08370adff" counter packets 0 bytes 0 drop # handle 47 + ip6 daddr fd00:28::2 iifname != "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 48 + ip6 daddr fd00:28::3 iifname != "br-ed1bb1cc9d39" counter packets 0 bytes 0 drop # handle 49 + ip6 daddr fd00:20::2 iifname != "br-527a7cd84ff6" counter packets 0 bytes 0 drop # handle 50 + ip6 daddr fd00:31::2 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 51 + ip6 daddr fd00:31::3 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 52 + ip6 daddr fd00:31::4 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 53 + ip6 daddr fd00:31::5 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 54 + ip6 daddr fd00:31::6 iifname != "br-f3f5c7cfa74e" counter packets 0 bytes 0 drop # handle 55 + ip6 daddr fd00:26::2 iifname != "br-263c8508de6f" counter packets 0 bytes 0 drop # handle 56 + ip6 daddr fd00:24::2 iifname != "br-f9343d8383ac" counter packets 0 bytes 0 drop # handle 57 + ip6 daddr fd00:1::3 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 58 + ip6 daddr fd00:1::4 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 59 + ip6 daddr fd00:41::2 iifname != "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 60 + ip6 daddr fd00:41::3 iifname != "br-051bbcfc21d8" counter packets 0 bytes 0 drop # handle 61 + ip6 daddr fd00:1::5 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 62 + ip6 daddr fd00:10::2 iifname != "br-bca9ecea74f1" counter packets 0 bytes 0 drop # handle 63 + ip6 daddr fd00:16::2 iifname != "br-618855db6757" counter packets 0 bytes 0 drop # handle 64 + ip6 daddr fd00:1::6 iifname != "br-5a9988b94efd" counter packets 0 bytes 0 drop # handle 65 + ip6 daddr fd00:17::2 iifname != "br-28584d053169" counter packets 0 bytes 0 drop # handle 66 + ip6 daddr fd00:18::2 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 67 + ip6 daddr fd00:18::3 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 68 + ip6 daddr fd00:18::4 iifname != "br-4ecbee63f4b0" counter packets 0 bytes 0 drop # handle 69 + ip6 daddr fd00:9::2 iifname != "br-8200b83f03e9" counter packets 0 bytes 0 drop # handle 71 + } +} diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-state.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-state.txt new file mode 100644 index 0000000..8164c0c --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-state.txt @@ -0,0 +1,21 @@ +2026-07-12T20:24:21-04:00 +TITANSERVER +Linux TITANSERVER 7.1.3-zen1-2-zen #1 ZEN SMP PREEMPT_DYNAMIC Thu, 09 Jul 2026 20:39:38 +0000 x86_64 GNU/Linux + +=== user@1200 === +LoadState=loaded +ActiveState=inactive +SubState=dead +FragmentPath=/usr/lib/systemd/system/user@.service +DropInPaths=/usr/lib/systemd/system/user@.service.d/10-login-barrier.conf +Result=success + +=== linger === +linger=absent + +=== UID 1200 processes === +none + +=== runtime directory/socket === +stat: cannot statx '/run/user/1200': No such file or directory +stat: cannot statx '/run/user/1200/docker.sock': No such file or directory diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-versions.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-versions.txt new file mode 100644 index 0000000..442d11f --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/runtime-versions.txt @@ -0,0 +1,41 @@ +docker 1:29.6.1-1 +containerd 2.3.2-1 +docker-compose 5.3.1-1 +rootlesskit 3.0.1-1 +slirp4netns 1.3.4-1 +libslirp 4.9.3-1 +openai-codex 0.144.1-2 +Client: + Version: 29.6.1 + API version: 1.55 + Go version: go1.26.4-X:nodwarf5 + Git commit: 8900f1d330 + Built: Sun Jun 28 16:44:00 2026 + OS/Arch: linux/amd64 + Context: default + +Server: + Engine: + Version: 29.6.1 + API version: 1.55 (minimum version 1.40) + Go version: go1.26.4-X:nodwarf5 + Git commit: 8ec5ab355a + Built: Sun Jun 28 16:44:00 2026 + OS/Arch: linux/amd64 + Experimental: false + containerd: + Version: v2.3.2 + GitCommit: fff62f14765df376e5fc36f5a8f8e795b5670f61.m + nvidia: + Version: 1.5.0 + GitCommit: + docker-init: + Version: 0.19.0 + GitCommit: de40ad0 +rootlesskit version 3.0.1 +slirp4netns version 1.3.4 +commit: unknown +libslirp: 4.9.3 +SLIRP_CONFIG_VERSION_MAX: 6 +libseccomp: 2.6.0 +codex-cli 0.144.1 diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-acls.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-acls.txt new file mode 100644 index 0000000..29ec4a6 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-acls.txt @@ -0,0 +1,32 @@ +user::rwx +group::r-x +other::--- + +user::rwx +group::--- +other::--- + +user::rwx +group::--- +other::--- + +user::r-x +group::r-x +other::--- + +user::rwx +group::r-x +other::--- + +user::rwx +group::--- +other::--- + +user::rwx +group::--- +other::--- + +user::rwx +group::--- +other::--- + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-mounts.txt b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-mounts.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-tree.psv b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-tree.psv new file mode 100644 index 0000000..4888aaf --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/inventory/workspace-tree.psv @@ -0,0 +1,8 @@ +/srv/le-app-codex|uid=0|gid=1200|mode=750|type=d +/srv/le-app-codex/home|uid=1200|gid=1200|mode=700|type=d +/srv/le-app-codex/lasereverything.net.db|uid=1200|gid=1200|mode=700|type=d +/srv/le-app-codex/database-source-readonly|uid=0|gid=1200|mode=550|type=d +/srv/le-app-codex/nonproduction-secrets|uid=0|gid=1200|mode=750|type=d +/srv/le-app-codex/container-runtime|uid=1200|gid=1200|mode=700|type=d +/srv/le-app-codex/testing-data|uid=1200|gid=1200|mode=700|type=d +/srv/le-app-codex/build-artifacts|uid=1200|gid=1200|mode=700|type=d diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/le-app-codex-runtime/resolv.conf b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/le-app-codex-runtime/resolv.conf new file mode 100644 index 0000000..0dba9d5 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/le-app-codex-runtime/resolv.conf @@ -0,0 +1,4 @@ +# Phase 2B IPv4-only public DNS; no LAN resolver exception. +nameserver 9.9.9.9 +nameserver 149.112.112.112 +options edns0 trust-ad diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/nftables.d/50-le-app-codex.nft b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/nftables.d/50-le-app-codex.nft new file mode 100644 index 0000000..9e7e3d7 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/nftables.d/50-le-app-codex.nft @@ -0,0 +1,54 @@ +table inet le_app_codex { + set approved_dns4 { + type ipv4_addr + flags interval + elements = { 9.9.9.9, 149.112.112.112 } + } + + set operator_host_public4 { + type ipv4_addr + flags interval + elements = { 74.67.173.56 } + } + + set denied4 { + type ipv4_addr + flags interval + elements = { + 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, + 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, + 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, + 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, + 224.0.0.0/4, 240.0.0.0/4 + } + } + + chain input { + type filter hook input priority filter; policy accept; + iifname "lecodex-host" ct state established,related accept + iifname "lecodex-host" ip daddr @approved_dns4 udp dport 53 accept + iifname "lecodex-host" ip daddr @approved_dns4 tcp dport 53 accept + iifname "lecodex-host" counter drop + } + + chain forward { + type filter hook forward priority filter; policy accept; + oifname "lecodex-host" ct state established,related accept + oifname "lecodex-host" counter drop + iifname "lecodex-host" ip daddr @approved_dns4 udp dport 53 accept + iifname "lecodex-host" ip daddr @approved_dns4 tcp dport 53 accept + iifname "lecodex-host" ip daddr @operator_host_public4 counter drop + iifname "lecodex-host" ip daddr @denied4 counter drop + iifname "lecodex-host" meta nfproto ipv6 counter drop + iifname "lecodex-host" tcp dport { 25, 465, 587 } counter drop + iifname "lecodex-host" tcp dport { 80, 443 } accept + iifname "lecodex-host" counter drop + } +} + +table ip le_app_codex_nat { + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 10.200.120.0/30 oifname != "lecodex-host" masquerade + } +} diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/le-app-codex-netns.service b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/le-app-codex-netns.service new file mode 100644 index 0000000..3d69517 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/le-app-codex-netns.service @@ -0,0 +1,11 @@ +[Unit] +Description=Root-owned IPv4-only namespace for le_app_codex +Before=user@1200.service +ConditionPathExists=/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/libexec/le-app-codex-netns up +ExecStop=/usr/local/libexec/le-app-codex-netns down + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf new file mode 100644 index 0000000..f2d5259 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf @@ -0,0 +1,7 @@ +[Slice] +CPUQuota=600% +MemoryHigh=48G +MemoryMax=64G +MemorySwapMax=16G +TasksMax=4096 + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf new file mode 100644 index 0000000..8155766 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf @@ -0,0 +1,40 @@ +[Unit] +Requires=le-app-codex-netns.service +After=le-app-codex-netns.service + +[Service] +NetworkNamespacePath=/run/netns/le-app-codex +ProtectSystem=strict +PrivateTmp=yes +ProtectHome=yes +ProtectProc=invisible +ProcSubset=all +BindReadOnlyPaths=/etc/le-app-codex-runtime/resolv.conf:/etc/resolv.conf +TemporaryFileSystem=/srv:ro +BindPaths=/srv/le-app-codex:/srv/le-app-codex +TemporaryFileSystem=/var:ro +TemporaryFileSystem=/mnt:ro +TemporaryFileSystem=/media:ro +TemporaryFileSystem=/opt:ro +InaccessiblePaths=/var/www +InaccessiblePaths=/root +InaccessiblePaths=/home +InaccessiblePaths=/boot +InaccessiblePaths=/efi +InaccessiblePaths=/run/docker.sock +InaccessiblePaths=/var/run/docker.sock +InaccessiblePaths=/run/containerd/containerd.sock +InaccessiblePaths=/run/podman +InaccessiblePaths=/run/dbus/system_bus_socket +InaccessiblePaths=/run/systemd/private +InaccessiblePaths=/var/lib/docker +InaccessiblePaths=/var/lib/containerd +InaccessiblePaths=/var/lib/containers +InaccessiblePaths=/etc/docker +InaccessiblePaths=/etc/containers +InaccessiblePaths=/etc/ssh +InaccessiblePaths=/etc/NetworkManager/system-connections +InaccessiblePaths=/etc/wireguard +DevicePolicy=closed +DeviceAllow=/dev/fuse rw +DeviceAllow=/dev/net/tun rw diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/docker/daemon.json b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/docker/daemon.json new file mode 100644 index 0000000..a10aef0 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/docker/daemon.json @@ -0,0 +1,16 @@ +{ + "data-root": "/srv/le-app-codex/container-runtime/docker", + "storage-driver": "fuse-overlayfs", + "live-restore": false, + "userland-proxy": false, + "ipv6": false, + "ip6tables": false, + "iptables": true, + "hosts": ["unix:///run/user/1200/docker.sock"], + "log-driver": "local", + "log-opts": { + "max-size": "20m", + "max-file": "5" + } +} + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/systemd/user/docker.service b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/systemd/user/docker.service new file mode 100644 index 0000000..1377a8c --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.config/systemd/user/docker.service @@ -0,0 +1,34 @@ +[Unit] +Description=Laser Everything rootless Docker +Documentation=https://docs.docker.com/engine/security/rootless/ + +[Service] +Type=notify +NotifyAccess=all +KillMode=mixed +Environment=HOME=/srv/le-app-codex/home +Environment=XDG_RUNTIME_DIR=/run/user/1200 +Environment=DOCKER_HOST=unix:///run/user/1200/docker.sock +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=/run/user/1200/dockerd-rootless +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=builtin +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520 +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=true +Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=false +ExecStart=/usr/local/libexec/dockerd-rootless-29.6.1.sh +ExecReload=/usr/bin/kill -s HUP $MAINPID +Restart=always +RestartSec=2 +TimeoutStartSec=0 +TimeoutStopSec=120 +LimitNOFILE=infinity +LimitNPROC=infinity +LimitCORE=infinity +TasksMax=infinity +Delegate=yes + +[Install] +WantedBy=default.target + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/config.json b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/config.json new file mode 100644 index 0000000..28810dd --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/config.json @@ -0,0 +1,4 @@ +{ + "currentContext": "le-app-codex" +} + diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json new file mode 100644 index 0000000..b406843 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json @@ -0,0 +1,10 @@ +{ + "Name": "le-app-codex", + "Metadata": {}, + "Endpoints": { + "docker": { + "Host": "unix:///run/user/1200/docker.sock", + "SkipTLSVerify": false + } + } +} diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/dockerd-rootless-29.6.1.sh b/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/dockerd-rootless-29.6.1.sh new file mode 100644 index 0000000..1face2b --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/dockerd-rootless-29.6.1.sh @@ -0,0 +1,255 @@ +#!/bin/sh +# dockerd-rootless.sh executes dockerd in rootless mode. +# +# Usage: dockerd-rootless.sh [DOCKERD_OPTIONS] +# +# External dependencies: +# * newuidmap and newgidmap needs to be installed. +# * /etc/subuid and /etc/subgid needs to be configured for the current user. +# +# Recognized environment variables: +# * DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR: the rootlesskit state dir. +# * Defaults to "$XDG_RUNTIME_DIR/dockerd-rootless". +# * DOCKERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|pasta|gvisor-tap-vsock|lxc-user-nic): the rootlesskit network driver. +# * Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed, else "pasta", else "vpnkit", else "gvisor-tap-vsock". +# * DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=NUM: the MTU value for the rootlesskit network driver. +# * Defaults to 65520 for slirp4netns, pasta, and gvisor-tap-vsock. Defaults to 1500 for other rootlesskit network drivers. +# * DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|implicit|gvisor-tap-vsock): the rootlesskit port driver. +# * Defaults to "implicit" for "pasta", "builtin" for other rootlesskit network drivers. +# * DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false): whether to protect slirp4netns with a dedicated mount namespace. +# * Defaults to "auto". +# * DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false): whether to protect slirp4netns with seccomp. +# * Defaults to "auto". +# * DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=(true|false): prohibit connections to 127.0.0.1 on the host (including via 10.0.2.2, in the case of slirp4netns). +# * Defaults to "true". +# * DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(true|false): whether to launch rootlesskit with the "detach-netns" mode. +# The "detached-netns" mode accelerates `docker (pull|push|build)` and enables `docker run --net=host` +# Defaults to "true". Set this to false only when facing a compatibility issue. + +# To apply an environment variable via systemd, create ~/.config/systemd/user/docker.service.d/override.conf as follows, +# and run `systemctl --user daemon-reload && systemctl --user restart docker`: +# --- BEGIN --- +# [Service] +# Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta" +# Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit" +# --- END --- + +# Guide to choose the network driver and the port driver: +# +# Network driver | Port driver | Net throughput | Port throughput | Src IP | No SUID | Note +# -----------------|------------------|----------------|-----------------|--------|---------|--------------------------------------------------------- +# gvisor-tap-vsock | builtin | Slow | Fast ✅ | ✅ (*) | ✅ | Default when slirp4netns is not installed +# slirp4netns | builtin | Slow | Fast ✅ | ✅ (*) | ✅ | Default when slirp4netns is installed +# vpnkit | builtin | Slow | Fast ✅ | ✅ (*) | ✅ | Legacy +# gvisor-tap-vsock | gvisor-tap-vsock | Slow | Slow | ❌ | ✅ | Not recommended. Use `builtin` port driver instead. +# slirp4netns | slirp4netns | Slow | Slow | ✅ | ✅ | +# pasta | implicit | Slow | Fast ✅ | ✅ | ✅ | Experimental; Needs recent version of pasta (2023_12_04) +# lxc-user-nic | builtin | Fast ✅ | Fast ✅ | ✅ (*) | ❌ | Experimental +# (bypass4netns) | (bypass4netns) | Fast ✅ | Fast ✅ | ✅ | ✅ | (Not integrated to RootlessKit) +# +# (*) Applicable since RootlessKit v3.0. Also requires userland-proxy to be disabled. + +# See the documentation for the further information: https://docs.docker.com/go/rootless/ + +set -e -x +case "$1" in + "check" | "install" | "uninstall") + echo "Did you mean 'dockerd-rootless-setuptool.sh $@' ?" + exit 1 + ;; +esac +if ! [ -w "$XDG_RUNTIME_DIR" ]; then + echo "XDG_RUNTIME_DIR needs to be set and writable" + exit 1 +fi +if ! [ -d "$HOME" ]; then + echo "HOME needs to be set and exist." + exit 1 +fi + +mount_directory() { + if [ -z "$_DOCKERD_ROOTLESS_CHILD" ]; then + echo "mount_directory should be called from the child context. Otherwise data loss is at risk" >&2 + exit 1 + fi + + DIRECTORY="$1" + if [ ! -d "$DIRECTORY" ]; then + return + fi + + # Bind mount directory: this makes this directory visible to + # Dockerd, even if it is originally a symlink, given Dockerd does + # not always follow symlinks. Some directories might also be + # "copied-up", meaning that they will also be writable on the child + # namespace; this will be the case only if they are provided as + # --copy-up to the rootlesskit. + DIRECTORY_REALPATH=$(realpath "$DIRECTORY") + MOUNT_OPTIONS="${2:---bind}" + rm -rf "$DIRECTORY" + mkdir -p "$DIRECTORY" + mount $MOUNT_OPTIONS "$DIRECTORY_REALPATH" "$DIRECTORY" +} + +rootlesskit="" +for f in docker-rootlesskit rootlesskit; do + if command -v $f > /dev/null 2>&1; then + rootlesskit=$f + break + fi +done +if [ -z "$rootlesskit" ]; then + echo "rootlesskit needs to be installed" + exit 1 +fi + +: "${CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR:=$XDG_RUNTIME_DIR/containerd-rootless}" +if [ -e "$CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR" ]; then + # https://github.com/moby/moby/issues/52171 + echo "dockerd-rootless.sh conflicts with containerd-rootless.sh. Stop containerd-rootless.sh if it's running, and remove $CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR if it still exists." + exit 1 +fi + +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR:=$XDG_RUNTIME_DIR/dockerd-rootless}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_NET:=}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_MTU:=}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER:=}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX:=auto}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP:=auto}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK:=}" +: "${DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS:=true}" +net=$DOCKERD_ROOTLESS_ROOTLESSKIT_NET +port_driver=$DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER +mtu=$DOCKERD_ROOTLESS_ROOTLESSKIT_MTU +if [ -z "$net" ]; then + if command -v slirp4netns > /dev/null 2>&1; then + # If --netns-type is present in --help, slirp4netns is >= v0.4.0. + if slirp4netns --help | grep -qw -- --netns-type; then + net=slirp4netns + else + echo "slirp4netns found but seems older than v0.4.0. Checking for other network drivers." + fi + fi + if [ -z "$net" ]; then + if command -v pasta > /dev/null 2>&1; then + net=pasta + fi + fi + if [ -z "$net" ]; then + if command -v vpnkit > /dev/null 2>&1; then + net=vpnkit + fi + fi + if [ -z "$net" ]; then + net=gvisor-tap-vsock + fi +fi +if [ "$net" = host ]; then + echo "Unsupported RootlessKit network driver: $net" + exit 1 +fi +if [ -z "$mtu" ]; then + case "$net" in + slirp4netns | pasta | gvisor-tap-vsock) + mtu=65520 + ;; + *) + mtu=1500 + ;; + esac +fi +if [ -z "$port_driver" ]; then + if [ "$net" = pasta ]; then + port_driver=implicit + else + port_driver=builtin + fi +fi + +host_loopback="--disable-host-loopback" +if [ "$DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK" = "false" ]; then + host_loopback="" +fi + +dockerd="${DOCKERD:-dockerd}" + +if [ -z "$_DOCKERD_ROOTLESS_CHILD" ]; then + _DOCKERD_ROOTLESS_CHILD=1 + export _DOCKERD_ROOTLESS_CHILD + if [ "$(id -u)" = "0" ]; then + echo "This script must be executed as a non-privileged user" + exit 1 + fi + # `selinuxenabled` always returns false in RootlessKit child, so we execute `selinuxenabled` in the parent. + # https://github.com/rootless-containers/rootlesskit/issues/94 + if command -v selinuxenabled > /dev/null 2>&1 && selinuxenabled; then + _DOCKERD_ROOTLESS_SELINUX=1 + export _DOCKERD_ROOTLESS_SELINUX + fi + + case "$DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in + 1 | true) + DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="--detach-netns $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS" + ;; + 0 | false) + # NOP + ;; + *) + echo "Unknown DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS value: $DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" + exit 1 + ;; + esac + + # Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces. + # + # --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks + # * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the + # namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host + # (by either systemd-networkd or NetworkManager) + # * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace + exec $rootlesskit \ + --state-dir=$DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR \ + --net=$net --mtu=$mtu \ + --slirp4netns-sandbox=$DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX \ + --slirp4netns-seccomp=$DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP \ + $host_loopback --port-driver=$port_driver \ + --copy-up=/etc --copy-up=/run \ + --propagation=rslave \ + $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \ + "$0" "$@" +else + [ "$_DOCKERD_ROOTLESS_CHILD" = 1 ] + + # remove the symlinks for the existing files in the parent namespace if any, + # so that we can create our own files in our mount namespace. + rm -f /run/docker /run/containerd /run/xtables.lock + + if [ -n "$_DOCKERD_ROOTLESS_SELINUX" ]; then + # iptables requires /run in the child to be relabeled. The actual /run in the parent is unaffected. + # https://github.com/containers/podman/blob/e6fc34b71aa9d876b1218efe90e14f8b912b0603/libpod/networking_linux.go#L396-L401 + # https://github.com/moby/moby/issues/41230 + chcon system_u:object_r:iptables_var_run_t:s0 /run + fi + + if [ "$(stat -c %T -f /etc)" = "tmpfs" ] && [ -L "/etc/ssl" ]; then + # Workaround for "x509: certificate signed by unknown authority" on openSUSE Tumbleweed. + # https://github.com/rootless-containers/rootlesskit/issues/225 + mount_directory /etc/ssl "--rbind" + fi + + netns="/proc/self/ns/net" + case "$DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in + 1 | true) + netns="$ROOTLESSKIT_STATE_DIR/netns" + ;; + esac + # When running with --firewall-backend=nftables, IP forwarding needs to be enabled + # because the daemon won't enable it. IP forwarding is harmless in the rootless + # netns, there's only a single external interface and only Docker uses the netns. + # So, always enable IPv4 and IPv6 forwarding. But ignore failure to enable IPv6 + # forwarding, for hosts with IPv6 disabled. + nsenter -n"$netns" sysctl -w net.ipv4.ip_forward=1 + nsenter -n"$netns" sysctl -w net.ipv6.conf.all.forwarding=1 || true + + exec "$dockerd" "$@" +fi diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/le-app-codex-netns b/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/le-app-codex-netns new file mode 100644 index 0000000..eac025a --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/payload/usr/local/libexec/le-app-codex-netns @@ -0,0 +1,40 @@ +#!/bin/sh +set -eu + +NS=le-app-codex +HOST_IF=lecodex-host +NS_IF=lecodex-ns + +down() { + /usr/bin/nft delete table inet le_app_codex 2>/dev/null || true + /usr/bin/nft delete table ip le_app_codex_nat 2>/dev/null || true + /usr/bin/ip link delete "$HOST_IF" 2>/dev/null || true + /usr/bin/ip netns delete "$NS" 2>/dev/null || true +} + +case "${1:-}" in + up) + test "$(id -u)" -eq 0 + test -r /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED + /usr/bin/grep -q 'elements = { 9\.9\.9\.9, 149\.112\.112\.112 }' /etc/nftables.d/50-le-app-codex.nft + /usr/bin/grep -q 'elements = { 74\.67\.173\.56 }' /etc/nftables.d/50-le-app-codex.nft + test ! -e /run/netns/$NS + ! /usr/bin/nft list table inet le_app_codex >/dev/null 2>&1 + ! /usr/bin/nft list table ip le_app_codex_nat >/dev/null 2>&1 + trap down EXIT HUP INT TERM + /usr/bin/ip netns add "$NS" + /usr/bin/ip link add "$HOST_IF" type veth peer name "$NS_IF" + /usr/bin/ip link set "$NS_IF" netns "$NS" + /usr/bin/ip address add 10.200.120.1/30 dev "$HOST_IF" + /usr/bin/ip link set "$HOST_IF" up + /usr/bin/ip -n "$NS" link set lo up + /usr/bin/ip -n "$NS" address add 10.200.120.2/30 dev "$NS_IF" + /usr/bin/ip -n "$NS" link set "$NS_IF" up + /usr/bin/ip -n "$NS" route add default via 10.200.120.1 dev "$NS_IF" + /usr/bin/ip netns exec "$NS" /usr/bin/sysctl -q -w net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1 + /usr/bin/nft -f /etc/nftables.d/50-le-app-codex.nft + trap - EXIT HUP INT TERM + ;; + down) down ;; + *) echo 'usage: le-app-codex-netns up|down' >&2; exit 64 ;; +esac diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-read-only-preflight.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-read-only-preflight.sh new file mode 100644 index 0000000..fdcd11a --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-read-only-preflight.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu +test "$(id -u)" -eq 0 +test "$(docker version --format '{{.Server.Version}}')" = 29.6.1 +test "$(sha256sum payload/usr/local/libexec/dockerd-rootless-29.6.1.sh | cut -d' ' -f1)" = 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46 +test ! -S /run/user/1200/docker.sock +test "$(systemctl show user@1200.service -p ActiveState --value)" = inactive +test ! -e /var/lib/systemd/linger/le_app_codex +test -z "$(pgrep -u 1200 || true)" +test ! -e /run/netns/le-app-codex +test ! -e /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf +test ! -e /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf +docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}} {{end}}' +ip -brief address +ip -4 route show table all +ip -6 route show table all +sed -n '1,120p' /etc/resolv.conf +nft --numeric list ruleset +systemctl cat user@1200.service +loginctl show-user 1200 || true +namei -om /srv/le-app-codex +find /srv/le-app-codex -xdev -printf '%M %u:%g %p\n' +getfacl -R -p /srv/le-app-codex +findmnt -R /srv/le-app-codex +grep -q 'elements = { 9\.9\.9\.9, 149\.112\.112\.112 }' payload/etc/nftables.d/50-le-app-codex.nft +grep -q 'elements = { 74\.67\.173\.56 }' payload/etc/nftables.d/50-le-app-codex.nft +grep -q '^nameserver 9\.9\.9\.9$' payload/etc/le-app-codex-runtime/resolv.conf +grep -q '^nameserver 149\.112\.112\.112$' payload/etc/le-app-codex-runtime/resolv.conf diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh new file mode 100644 index 0000000..d78a629 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +failures=0 +artifact_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } +run_check() { + label=$1 + shift + if "$@"; then pass "$label"; else fail "$label"; fi +} +expect_stat() { + path=$1 expected=$2 + actual=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + if [ "$actual" = "$expected" ]; then pass "$path ownership/mode"; else fail "$path expected $expected, got ${actual:-missing}"; fi +} +expect_absent() { + path=$1 + if [ ! -e "$path" ] && [ ! -L "$path" ]; then pass "$path absent"; else fail "$path must not already exist"; fi +} + +if [ "$(id -u)" = 0 ]; then pass 'running as root'; else fail 'must be run as root'; fi +if [ -n "$artifact_root" ] && [ -d "$artifact_root/payload" ]; then pass 'artifact root resolved'; else fail 'artifact root not found'; fi + +if (cd "$artifact_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'complete artifact manifest'; else fail 'complete artifact manifest'; fi +if (cd "$artifact_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS); then pass 'original root inventory'; else fail 'original root inventory'; fi + +run_check 'staged nftables syntax' /usr/bin/nft --check -f "$artifact_root/payload/etc/nftables.d/50-le-app-codex.nft" + +system_unit_path="$artifact_root/payload/etc/systemd/system:/etc/systemd/system:/run/systemd/system:/usr/local/lib/systemd/system:/usr/lib/systemd/system" +systemd_output=$(SYSTEMD_UNIT_PATH="$system_unit_path" /usr/bin/systemd-analyze verify --recursive-errors=no user@1200.service user-1200.slice le-app-codex-netns.service 2>&1) +systemd_rc=$? +printf '%s\n' "$systemd_output" +systemd_unexpected=$(printf '%s\n' "$systemd_output" | /usr/bin/sed -e '\#le-app-codex-netns.service: Command /usr/local/libexec/le-app-codex-netns is not executable: No such file or directory#d') +if [ "$systemd_rc" -eq 0 ] || [ -z "$systemd_unexpected" ]; then pass 'staged system units/drop-ins via host search path'; else fail 'staged system units/drop-ins via host search path'; fi + +user_unit_path="$artifact_root/payload/srv/le-app-codex/home/.config/systemd/user:/etc/systemd/user:/run/systemd/user:/usr/local/lib/systemd/user:/usr/lib/systemd/user" +user_systemd_output=$(SYSTEMD_UNIT_PATH="$user_unit_path" /usr/bin/systemd-analyze verify --recursive-errors=no docker.service 2>&1) +user_systemd_rc=$? +printf '%s\n' "$user_systemd_output" +user_systemd_unexpected=$(printf '%s\n' "$user_systemd_output" | /usr/bin/sed -e '\#docker.service: Command /usr/local/libexec/dockerd-rootless-29.6.1.sh is not executable: No such file or directory#d') +if [ "$user_systemd_rc" -eq 0 ] || [ -z "$user_systemd_unexpected" ]; then pass 'staged Docker user unit via host search path'; else fail 'staged Docker user unit via host search path'; fi + +shell_failures=0 +for file in "$artifact_root"/tests/*.sh "$artifact_root"/payload/usr/local/libexec/*; do + /bin/sh -n "$file" || shell_failures=$((shell_failures + 1)) +done +if [ "$shell_failures" -eq 0 ]; then pass 'shell syntax'; else fail 'shell syntax'; fi + +json_failures=0 +for file in "$artifact_root"/payload/srv/le-app-codex/home/.config/docker/daemon.json \ + "$artifact_root"/payload/srv/le-app-codex/home/.docker/config.json \ + "$artifact_root"/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json \ + "$artifact_root"/inventory/docker-networks.json; do + /usr/bin/jq empty "$file" || json_failures=$((json_failures + 1)) +done +if [ "$json_failures" -eq 0 ]; then pass 'JSON syntax'; else fail 'JSON syntax'; fi + +expect_stat /srv/le-app-codex '0:1200:750:directory' +expect_stat /srv/le-app-codex/home '1200:1200:700:directory' +expect_stat /srv/le-app-codex/lasereverything.net.db '1200:1200:700:directory' +expect_stat /srv/le-app-codex/database-source-readonly '0:1200:550:directory' +expect_stat /srv/le-app-codex/nonproduction-secrets '0:1200:750:directory' +expect_stat /srv/le-app-codex/container-runtime '1200:1200:700:directory' +expect_stat /srv/le-app-codex/testing-data '1200:1200:700:directory' +expect_stat /srv/le-app-codex/build-artifacts '1200:1200:700:directory' + +project_mounts=$(/usr/bin/findmnt -rn -o TARGET | /usr/bin/awk '$0 == "/srv/le-app-codex" || index($0, "/srv/le-app-codex/") == 1 { print }') +if [ -z "$project_mounts" ]; then pass 'no project-rooted mounts'; else fail "unexpected project-rooted mounts: $project_mounts"; fi + +expect_absent /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED +expect_absent /etc/le-app-codex-runtime/resolv.conf +expect_absent /etc/nftables.d/50-le-app-codex.nft +expect_absent /etc/systemd/system/le-app-codex-netns.service +expect_absent /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf +expect_absent /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf +expect_absent /usr/local/libexec/dockerd-rootless-29.6.1.sh +expect_absent /usr/local/libexec/le-app-codex-netns +expect_absent /srv/le-app-codex/home/.config/systemd/user/docker.service +expect_absent /srv/le-app-codex/home/.config/docker/daemon.json +expect_absent /srv/le-app-codex/home/.docker/config.json +expect_absent /srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json +expect_absent /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh +expect_absent /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh +expect_absent /srv/le-app-codex/phase2b-tests/10-inert-containment.sh +expect_absent /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh +expect_absent /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh + +active_state=$(/usr/bin/systemctl show user@1200.service -p ActiveState --value 2>/dev/null) +if [ "$active_state" = inactive ]; then pass 'user@1200.service inactive'; else fail "user@1200.service state is ${active_state:-unknown}"; fi +if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi +uid_processes=$(/usr/bin/pgrep -u 1200 2>/dev/null) +if [ -z "$uid_processes" ]; then pass 'UID 1200 has no processes'; else fail "UID 1200 processes found: $uid_processes"; fi +if [ ! -S /run/user/1200/docker.sock ]; then pass 'rootless Docker socket absent'; else fail 'rootless Docker socket exists'; fi + +public_ipv4=$(/usr/bin/curl -4fsS --max-time 10 https://api.ipify.org 2>/dev/null) +if [ "$public_ipv4" = 74.67.173.56 ]; then pass 'public/NAT IPv4 unchanged'; else fail "public/NAT IPv4 drift or lookup failure: ${public_ipv4:-unavailable}"; fi + +printf 'root pre-install validation failures=%s\n' "$failures" +[ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/10-inert-containment.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/10-inert-containment.sh new file mode 100644 index 0000000..30b862a --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/10-inert-containment.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -eu +fail=0 +expect_denied() { if "$@" >/dev/null 2>&1; then echo "UNEXPECTED SUCCESS: $*" >&2; fail=1; fi; } + +test "$(id -u)" -eq 1200 +test "$DOCKER_HOST" = unix:///run/user/1200/docker.sock +touch /srv/le-app-codex/tmp/phase2b-write-test +rm /srv/le-app-codex/tmp/phase2b-write-test +expect_denied ls /var/www +expect_denied ls /root +expect_denied ls /home +expect_denied ls /srv/directus-archive +expect_denied ls /srv/codex-work +expect_denied stat /run/docker.sock +expect_denied stat /var/run/docker.sock +expect_denied stat /run/containerd/containerd.sock +expect_denied stat /dev/sda +expect_denied stat /proc/1/root +expect_denied sh -c 'echo x > /etc/phase2b-write-test' +expect_denied sh -c 'echo x > /srv/phase2b-write-test' +expect_denied curl -4fsS --connect-timeout 2 http://127.0.0.1 +expect_denied curl -4fsS --connect-timeout 2 http://10.200.120.1 +expect_denied curl -4fsS --connect-timeout 2 http://192.168.10.151 +expect_denied nc -w2 -z 192.168.10.1 53 +expect_denied curl -4fsS --connect-timeout 2 http://10.98.0.2 +expect_denied curl -4fsS --connect-timeout 2 http://74.67.173.56 +expect_denied curl -4fsS --connect-timeout 2 http://169.254.169.254/latest/meta-data/ +expect_denied curl -6fsS --connect-timeout 2 https://[::1]/ +expect_denied nc -w2 -z 1.1.1.1 25 +expect_denied nc -w2 -z 1.1.1.1 465 +expect_denied nc -w2 -z 1.1.1.1 587 +test "$(ps -e --no-headers | wc -l)" -lt 32 +test -w /dev/fuse +test -w /dev/net/tun +grep -q '^nameserver 9\.9\.9\.9$' /etc/resolv.conf +grep -q '^nameserver 149\.112\.112\.112$' /etc/resolv.conf +getent ahostsv4 example.com >/dev/null +test -r /sys/fs/cgroup/cpu.max +test -r /sys/fs/cgroup/memory.max +exit "$fail" diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/20-rootless-docker.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/20-rootless-docker.sh new file mode 100644 index 0000000..5767f55 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/20-rootless-docker.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu +fail=0 +export HOME=/srv/le-app-codex/home +export DOCKER_HOST=unix:///run/user/1200/docker.sock +ALPINE='docker.io/library/alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659' +NGINX='docker.io/library/nginx:1.29.4-alpine@sha256:4870c12cd2ca986de501a804b4f506ad3875a0b1874940ba0a2c7f763f1855b2' +test "$(id -u)" -eq 1200 +test -S /run/user/1200/docker.sock +test "$(docker context show)" = le-app-codex +test "$(docker context inspect le-app-codex --format '{{.Endpoints.docker.Host}}')" = unix:///run/user/1200/docker.sock +docker info --format '{{json .SecurityOptions}}' | grep -q rootless +test "$(docker info --format '{{.DockerRootDir}}')" = /srv/le-app-codex/container-runtime/docker +docker run --rm --network none "$ALPINE" true +docker run --rm "$ALPINE" wget -qO- https://example.com >/dev/null +for target in 127.0.0.1 10.200.120.1 192.168.10.1 192.168.10.151 10.98.0.2 74.67.173.56 169.254.169.254; do + if docker run --rm "$ALPINE" wget -T2 -qO- "http://$target" >/dev/null 2>&1; then + echo "UNEXPECTED CONTAINER ACCESS: $target" >&2; fail=1 + fi +done +if docker run --rm "$ALPINE" sh -c 'nc -w2 -z 1.1.1.1 25 || nc -w2 -z 1.1.1.1 465 || nc -w2 -z 1.1.1.1 587'; then + echo 'UNEXPECTED CONTAINER SMTP ACCESS' >&2; fail=1 +fi +cid=$(docker run -d --rm -p 18080:80 "$NGINX") +trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT +sleep 2 +for host in 127.0.0.1 192.168.10.151 10.98.0.2; do + if curl -fsS --connect-timeout 2 "http://$host:18080" >/dev/null 2>&1; then + echo "UNEXPECTED PUBLISHED PORT: $host" >&2; fail=1 + fi +done +docker rm -f "$cid" >/dev/null +trap - EXIT +test ! -S /run/docker.sock || ! docker -H unix:///run/docker.sock info >/dev/null 2>&1 +exit "$fail" diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/run-inert-without-user-manager.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/run-inert-without-user-manager.sh new file mode 100644 index 0000000..7eb7aa5 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/run-inert-without-user-manager.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu +test "$(id -u)" -eq 0 +test "$(systemctl is-active user@1200.service || true)" = inactive +systemd-run --wait --pipe --collect --unit=le-app-codex-inert-test \ + --uid=1200 --gid=1200 \ + --setenv=DOCKER_HOST=unix:///run/user/1200/docker.sock \ + --property=NetworkNamespacePath=/run/netns/le-app-codex \ + --property=ProtectSystem=strict --property=PrivateTmp=yes \ + --property=ProtectHome=yes --property=ProtectProc=invisible \ + --property=TemporaryFileSystem=/srv:ro \ + --property=BindPaths=/srv/le-app-codex:/srv/le-app-codex \ + --property=TemporaryFileSystem=/var:ro \ + --property=TemporaryFileSystem=/mnt:ro \ + --property=TemporaryFileSystem=/media:ro \ + --property=TemporaryFileSystem=/opt:ro \ + --property=DevicePolicy=closed --property='DeviceAllow=/dev/fuse rw' \ + --property='DeviceAllow=/dev/net/tun rw' \ + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh + From df6b53e63916880bec2c77219b04b010b8b453f1 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 22:34:04 -0400 Subject: [PATCH 11/14] harden Phase 2B Stage 1 installation state --- .../INSTALL-ORDER.md | 8 +- .../phase2b-contained-runtime/MANIFEST.sha256 | 14 +- .../phase2b-contained-runtime/README.md | 4 +- .../phase2b-contained-runtime/ROLLBACK.md | 11 +- .../STATIC-SAFETY-AUDIT.md | 3 + .../tests/00-root-preinstall-validate.sh | 9 +- .../tests/30-install-stage1.sh | 280 +++++++++++++++++ .../tests/31-rollback-stage1.sh | 283 ++++++++++++++++++ .../tests/32-stage1-state-tests.sh | 283 ++++++++++++++++++ .../tests/stage1-state-lib.sh | 187 ++++++++++++ 10 files changed, 1062 insertions(+), 20 deletions(-) create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/30-install-stage1.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/31-rollback-stage1.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/32-stage1-state-tests.sh create mode 100644 docs/le-app-database-migration/phase2b-contained-runtime/tests/stage1-state-lib.sh diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md b/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md index c82dc16..26ad416 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md +++ b/docs/le-app-database-migration/phase2b-contained-runtime/INSTALL-ORDER.md @@ -2,11 +2,11 @@ Every numbered boundary requires review and explicit approval. Nothing here authorizes execution. -1. From this directory, run only `tests/00-root-preinstall-validate.sh` as root and require zero failures. It atomically checks both manifests, staged nftables/systemd/shell/JSON, exact Phase 1 ownership/modes, absent approval marker/installed payload/manager/process/socket/linger, and public/NAT address drift. Save output outside production. +1. Review the passing non-mutating `tests/32-stage1-state-tests.sh` results and the shared state implementation in `tests/stage1-state-lib.sh`. Then, from this directory, run only `tests/30-install-stage1.sh` as root and require zero failures. It invokes `tests/00-root-preinstall-validate.sh`, checks both manifests, requires all 16 destination files absent, validates shared parents, installs only static Stage 1 files, and atomically advances immutable checksummed state generations from `IN_PROGRESS` to `COMPLETE`. It does not create the approval marker or perform any reload/start/network action. 2. Preserve the approved Quad9 DNS pair `9.9.9.9` and `149.112.112.112`, observed public/NAT address denial `74.67.173.56`, and verified root inventory. Quad9 requires no LAN exception; `192.168.10.1` remains denied. Any different value or public/NAT drift requires artifact edits, a new manifest, and another review. -3. Copy only the manifest-listed launcher to `/usr/local/libexec`, mode `0755`, `root:root`; rehash the installed file. -4. Copy only the slice and `user@1200.service` drop-ins, resolver file, namespace helper/unit, and nftables fragment to their manifest paths with `root:root`, directories/files `0755/0644`, helper `0755`. After exact-value approval, create root-owned mode `0644` `/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED` containing the artifact-manifest checksum and approval timestamp. Do not start anything. Run `systemd-analyze verify` and `nft --check -f`; inspect the merged `systemctl cat`/properties. -5. Copy the Docker user unit/config/context and tests below `/srv/le-app-codex`; set directories `0750`, config/unit/tests `0640` except tests `0750`, all owned `1200:1200`. Do not create credentials, checkout, or source copies. +3. Resolve `/var/lib/le-app-codex-phase2b/stage1/current` as a strict local generation name and review that generation's `manifest` and `manifest.sha256`; require `COMPLETE`, source commit, artifact-manifest digest, hostname, timestamp, 16 completed file records, no pending records, and every reached directory disposition to match the installer output. +4. Do not create `/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED` during Stage 1. Do not run `systemctl daemon-reload`, load nftables, create the namespace, or start/enable anything. +5. Review installed owner/mode/size/hash evidence. Docker user configuration and the five acceptance/preflight scripts must be owned by `1200:1200`; tests are `0750`, configs/unit `0640`. No credentials, checkout, or source copies are installed. 6. Run `systemctl daemon-reload`. Confirm the user manager remains inactive, lingering absent, and socket/process absent. 7. Start only `le-app-codex-netns.service`. Inspect namespace identity, IPv4-only addresses/routes, nftables counters/rules, host routing, and production Docker health. Stop on any drift. 8. Run `tests/run-inert-without-user-manager.sh`. This starts only a finite transient test process, never `user@1200.service`, Docker, or Codex. Require every filesystem/process/cgroup/device/socket/network assertion to pass. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 b/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 index e02d413..f640aee 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 +++ b/docs/le-app-database-migration/phase2b-contained-runtime/MANIFEST.sha256 @@ -1,8 +1,8 @@ f5bc35fd3767c0cbc72495b1dea283e8237789dd383ed754bf629ca09379ab97 .gitattributes -e4c381697c727f364c2a212fede2f703cfab8e0a57aaa4fa98550478a76eef55 INSTALL-ORDER.md -a62100ff7046a6e7d89adb8d2ab6bdf4ef7a72153df7cc439c8b00b42c7df347 README.md -b72075836eddd338eb1402beab775b4dc21a61d315e78cd8385f719578924293 ROLLBACK.md -b8a5f38f26fa76db6281a14de2baae3505cd78428b4c81a27a23ea89e256f321 STATIC-SAFETY-AUDIT.md +b0ee162a228f128774fdefcbbb603c98aadc145fbe2b5e9ee8fd0c1a2ba444b2 INSTALL-ORDER.md +e0182ff047fe6f8d71fc0a21fd73be3ced33b3f9fed990b902dd5f76a2f0a53a README.md +4f3f30e0f4c45f7ecfc6c6e169fda6b53b9eaa694f3ed26f818342fc80e7a446 ROLLBACK.md +2a2aed8538b36afeed20bb58ed4168b3f2d55b0ed20e28f6860a16714a6df979 STATIC-SAFETY-AUDIT.md 05bdf987ad0049118acf9371c6affae0d7bf65bfcb869d297986860ee20dfac4 inventory/SHA256SUMS e6f6da94601927a23d252f32de93e92c99d31fb2d2cbd9b00f2b88224dbb684e inventory/docker-networks.json 9a5d343092c8cf473f0e4d06cb6848f1788aa8bc41b1b3fb7412faea7ea7b48c inventory/docker-networks.psv @@ -25,7 +25,11 @@ ea0c280203614c3e308013a8d51271bfa8adafd517f65cdf3224d2385244af6d payload/etc/sy 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46 payload/usr/local/libexec/dockerd-rootless-29.6.1.sh 9aed833454a78dc99af6c18da1cf09660ef717964adfd309f61f043b53c05464 payload/usr/local/libexec/le-app-codex-netns 4f94d88283207b2bfa849f4e5b126efa8872be97e59edd1f700b1b0a80623035 tests/00-read-only-preflight.sh -f7629ad1178720c6046bf73d2babdcf7f8db6398ded172c5a088340f38d077a2 tests/00-root-preinstall-validate.sh +c540a7b6c3cdd545e8cf66deb848f79bd2b1cf0c0c83728aa9f37548289fe94f tests/00-root-preinstall-validate.sh 9a573d539da46fbd683da60920596118174fdcf053f79f037ba21aee2deab82d tests/10-inert-containment.sh 0ec4d5d955e55fa8eb053fe2efa9a60509ada31afa48b71b29652dfacbf4d1c2 tests/20-rootless-docker.sh +bfacb009619c43b03e57834715fa625ce00c13a5b431396d68401916c31cb3a8 tests/30-install-stage1.sh +954979e35b6f39531ce4d8395d55f011cbcfec16eaeae941b6a73b732569e029 tests/31-rollback-stage1.sh +a9d235f4f5c1dd36ce50579344d099b5a41bf11514888de8ae8d35875f741aa4 tests/32-stage1-state-tests.sh 1cf5dfa80b8d00de3ef1a47d84c30a891f4c6192345d5fc9247c4ab7d4f40f31 tests/run-inert-without-user-manager.sh +e8d7b15d10e66fc050eabc758338ec928d0f82af39ee075810865435f726db74 tests/stage1-state-lib.sh diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/README.md b/docs/le-app-database-migration/phase2b-contained-runtime/README.md index cc2cdf7..903ecce 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/README.md +++ b/docs/le-app-database-migration/phase2b-contained-runtime/README.md @@ -40,6 +40,8 @@ There are no remaining documentation sentinels. The operator approved Quad9's ma ## Validation status -The root pre-install validator passed on TITANSERVER with zero failures: both manifests, staged nftables syntax, staged systemd units/drop-ins, shell/JSON syntax, Phase 1 ownership/modes, mount absence, installed-path absence, inactive user manager, absent linger/process/socket, and unchanged public/NAT IPv4 all passed. Missing `/usr/local/libexec` warnings were expected for the uninstalled payload. +The committed review set's root pre-install validator passed on TITANSERVER with zero failures: both manifests, staged nftables syntax, staged systemd units/drop-ins, shell/JSON syntax, Phase 1 ownership/modes, mount absence, installed-path absence, inactive user manager, absent linger/process/socket, and unchanged public/NAT IPv4 all passed. Missing `/usr/local/libexec` warnings were expected for the uninstalled payload. That host run predates the uncommitted Stage 1 recovery amendments; the future installer reruns the root validator against the then-current committed bytes before copying anything. + +Stage 1 installation and rollback are repository-native scripts, not pasted heredocs. `tests/30-install-stage1.sh` installs exactly the 16 approved static files and validates shared parents without changing them. State uses immutable, verified generation directories under `/var/lib/le-app-codex-phase2b/stage1/generations`; a root-owned regular `current` pointer is the single atomic commit point. `IN_PROGRESS` is durable before payload copying and after every directory/file transition; `COMPLETE` requires exactly all 16 files and no pending record. `tests/31-rollback-stage1.sh` uses the strict parser in `tests/stage1-state-lib.sh`, supports both statuses, journals rollback with an atomic transaction-directory tombstone, removes only recorded matching objects, and preserves retryable state on payload or directory failure. The non-mutating `tests/32-stage1-state-tests.sh` exercises the shared parser, publication failpoints, pointer/generation rejection, partial and complete rollback simulations, and failure preservation in a temporary fake filesystem. Neither operational script creates the approval marker, reloads systemd, loads nftables, creates a namespace, or starts/enables anything. See `STATIC-SAFETY-AUDIT.md`, `INSTALL-ORDER.md`, `ROLLBACK.md`, `tests/`, `inventory/`, and `MANIFEST.sha256`. Any edit invalidates the manifest and requires another review. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md b/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md index 023438f..2605cd7 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md +++ b/docs/le-app-database-migration/phase2b-contained-runtime/ROLLBACK.md @@ -2,11 +2,8 @@ Do not remove Phase 1 identity/workspace, Phase 2A packages, kernels, snapshots, production resources, or unrelated nftables objects. -1. Stop `user@1200.service`; confirm UID 1200 has no processes. This is the only user-manager stop in scope. -2. Stop `le-app-codex-netns.service`. Its `ExecStop` removes only `lecodex-host` and `/run/netns/le-app-codex` and deletes only the two `le_app_codex*` nftables tables. -3. Remove only the Phase 2B files enumerated in `MANIFEST.sha256` and `/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED`, preserving the pre-existing `/srv/le-app-codex` skeleton. -4. Run `systemctl daemon-reload` and `systemctl reset-failed le-app-codex-netns.service`. -5. Confirm there is no UID 1200 process, socket, namespace, veth, project nftables table, containment/resource drop-in, launcher, Docker user unit, daemon/client config, or Phase 2B test copy. -6. Confirm lingering is still disabled and production/rootful Docker remains healthy and unchanged. +For Stage 1 rollback, run only `tests/31-rollback-stage1.sh` from a clean artifact worktree whose `MANIFEST.sha256` digest matches the authoritative state generation. The current branch may have later unrelated commits. Rollback accepts verified `IN_PROGRESS` or `COMPLETE`, uses the allowlist parser shared through `tests/stage1-state-lib.sh`, and refuses operation unless the approval marker, user manager, UID-1200 processes, socket, namespace, and project nftables tables are absent. Before removal it atomically renames the whole Stage 1 transaction subtree to a retryable cleanup tombstone. A retry accepts already-removed entries and re-verifies and removes any matching entries that remain. It removes only recorded matching payload/temporary files and created or pending directories; a `COMPLETE` record requires all 16 files. Shared parents recorded as pre-existing are never removed or altered. Immutable authoritative state remains intact through all payload and directory removals and while unreferenced state is cleaned; its manifest and checksum are removed only in the final cleanup phase. -If a file existed before Phase 2B, stop: this rollback package intentionally has no overwrite/restore path because installation must have failed closed on pre-existence. +Later-stage rollback, after explicit runtime activation, is separate: stop the user manager and namespace first, then verify the runtime is inert before using a stage-appropriate reviewed rollback. Stage 1 rollback must never be used after the approval marker is created or any namespace/firewall/runtime component has been activated. + +If a destination file existed before Phase 2B, stop: Stage 1 has no overwrite/restore path and fails closed on pre-existence. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md b/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md index 53bcec9..fe0eca2 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md +++ b/docs/le-app-database-migration/phase2b-contained-runtime/STATIC-SAFETY-AUDIT.md @@ -13,5 +13,8 @@ Status: pass for final review; execution remains approval-gated. - **Existing nftables:** the verified ruleset has no `le_app_codex` objects. Dedicated tables avoid editing Docker-owned chains. Interface-scoped terminal drops remain effective alongside existing Docker base chains. - **Inventory:** the root record checksums pass. UID 1200 is inactive, not lingering, has no process/runtime/socket; project ACLs and modes match Phase 1; no project-rooted mounts exist; all 41 Docker networks fall within denied IPv4/IPv6 space. - **Rollback:** names and paths are Phase 2B-specific; teardown deletes only the project tables, namespace/veth, marker, and manifest-listed files. +- **Stage 1 installer:** installs exactly 16 allowlisted files; long Docker-context paths are single-line variables; existing shared parents are accepted only when real, root-owned, and not group/other writable; existing shared parent metadata is never changed. +- **Stage 1 state/rollback:** immutable mode `0600` manifest/checksum generations record commit, artifact-manifest digest, hashes, pending/completed files, pending/created/pre-existing directories, time, and host. A verified regular `current` pointer is switched by one atomic rename only after the new generation is synced and semantically validated. Rollback accepts `IN_PROGRESS` subsets or an exact 16-file `COMPLETE`, atomically tombstones the transaction before removal, re-verifies remaining objects on retry, and calls `rmdir` only for allowlisted recorded directories. +- **Crash recovery:** deterministic `.phase2b-new` payload temporaries are covered by `FILE_PENDING`; hidden incomplete state generations never become authoritative; old generations are retained. The parser shared by installer, rollback, and `tests/32-stage1-state-tests.sh` rejects blank, duplicate, unknown, malformed, unapproved, or escaping records. The state resolver separately rejects symlinked or escaping pointers/generations and unverified generation content. Publication failpoints cover initial and updated `IN_PROGRESS`, final `COMPLETE`, and both sides of the atomic pointer switch. Hard stops remain: changed Docker version/launcher hash, changed inventory, public/NAT address drift, missing approval marker at namespace-start time, pre-existing project tables/namespace, any acceptance failure, or any endpoint other than the single rootless Unix socket. `ProtectProc=invisible` remains an unproven runtime acceptance candidate. diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh index d78a629..73520b1 100644 --- a/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/00-root-preinstall-validate.sh @@ -44,7 +44,10 @@ if [ "$user_systemd_rc" -eq 0 ] || [ -z "$user_systemd_unexpected" ]; then pass shell_failures=0 for file in "$artifact_root"/tests/*.sh "$artifact_root"/payload/usr/local/libexec/*; do - /bin/sh -n "$file" || shell_failures=$((shell_failures + 1)) + case "$file" in + */30-install-stage1.sh|*/31-rollback-stage1.sh|*/32-stage1-state-tests.sh|*/stage1-state-lib.sh) /bin/bash -n "$file" || shell_failures=$((shell_failures + 1)) ;; + *) /bin/sh -n "$file" || shell_failures=$((shell_failures + 1)) ;; + esac done if [ "$shell_failures" -eq 0 ]; then pass 'shell syntax'; else fail 'shell syntax'; fi @@ -89,10 +92,10 @@ expect_absent /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh active_state=$(/usr/bin/systemctl show user@1200.service -p ActiveState --value 2>/dev/null) if [ "$active_state" = inactive ]; then pass 'user@1200.service inactive'; else fail "user@1200.service state is ${active_state:-unknown}"; fi -if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi +if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -L /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ] && [ ! -L /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi uid_processes=$(/usr/bin/pgrep -u 1200 2>/dev/null) if [ -z "$uid_processes" ]; then pass 'UID 1200 has no processes'; else fail "UID 1200 processes found: $uid_processes"; fi -if [ ! -S /run/user/1200/docker.sock ]; then pass 'rootless Docker socket absent'; else fail 'rootless Docker socket exists'; fi +if [ ! -e /run/user/1200/docker.sock ] && [ ! -L /run/user/1200/docker.sock ]; then pass 'rootless Docker socket absent'; else fail 'rootless Docker socket path exists'; fi public_ipv4=$(/usr/bin/curl -4fsS --max-time 10 https://api.ipify.org 2>/dev/null) if [ "$public_ipv4" = 74.67.173.56 ]; then pass 'public/NAT IPv4 unchanged'; else fail "public/NAT IPv4 drift or lookup failure: ${public_ipv4:-unavailable}"; fi diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/30-install-stage1.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/30-install-stage1.sh new file mode 100644 index 0000000..2596362 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/30-install-stage1.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash + +failures=0 +installed_count=0 +artifact_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +repo_root=$(CDPATH= cd -- "$artifact_root/../../.." 2>/dev/null && pwd) +payload_root=$artifact_root/payload +state_dir=/var/lib/le-app-codex-phase2b +state_transaction=/var/lib/le-app-codex-phase2b/stage1 +state_current=/var/lib/le-app-codex-phase2b/stage1/current +context_hash=1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535 +context_source=$payload_root/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +context_destination=/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +source "$artifact_root/tests/stage1-state-lib.sh" +stage1_records=() + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } + +safe_shared_directory() { + local path=$1 actual_uid actual_gid actual_mode + if [ ! -d "$path" ] || [ -L "$path" ]; then + fail "$path must be a real directory" + return 1 + fi + actual_uid=$(/usr/bin/stat -c '%u' "$path" 2>/dev/null) + actual_gid=$(/usr/bin/stat -c '%g' "$path" 2>/dev/null) + actual_mode=$(/usr/bin/stat -c '%a' "$path" 2>/dev/null) + if [ "$actual_uid" != 0 ] || [ "$actual_gid" != 0 ]; then + fail "$path must be root:root" + return 1 + fi + if [ $((8#$actual_mode & 0022)) -ne 0 ]; then + fail "$path must not be group/other writable" + return 1 + fi + pass "$path safe shared parent" +} + +exact_directory() { + local path=$1 uid=$2 gid=$3 mode=$4 kind=$5 actual + if [ -e "$path" ] || [ -L "$path" ]; then + if [ ! -d "$path" ] || [ -L "$path" ]; then + fail "$path exists but is not a real directory" + return 1 + fi + actual=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + if [ "$actual" != "$uid:$gid:${mode#0}:directory" ]; then + fail "$path expected $uid:$gid:${mode#0}:directory, got ${actual:-missing}" + return 1 + fi + stage1_records+=("DIR_EXISTING|$path|$uid|$gid|$mode|$kind") + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || fail "publish state after accepting $path" + pass "$path existing directory accepted" + else + stage1_records+=("DIR_PENDING|$path|$uid|$gid|$mode|$kind") + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || fail "publish state before creating $path" + if [ "$failures" -ne 0 ]; then return 1; fi + if /usr/bin/install -d -o "$uid" -g "$gid" -m "$mode" "$path"; then + stage1_records[${#stage1_records[@]}-1]="DIR_CREATED|$path|$uid|$gid|$mode|$kind" + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || fail "publish state after creating $path" + pass "$path created" + else + fail "$path creation" + return 1 + fi + fi +} + +expect_absent_file() { + local path=$1 + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + pass "$path absent" + else + fail "$path unexpectedly exists" + fi +} + +record_existing_base() { + local path=$1 kind=$2 uid gid mode + uid=$(/usr/bin/stat -c '%u' "$path" 2>/dev/null) + gid=$(/usr/bin/stat -c '%g' "$path" 2>/dev/null) + mode=$(/usr/bin/stat -c '%a' "$path" 2>/dev/null) + stage1_records+=("DIR_EXISTING|$path|$uid|$gid|0$mode|$kind") + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || fail "publish state after recording $path" +} + +verify_file() { + local source_file=$1 destination=$2 uid=$3 gid=$4 mode=$5 + local source_hash destination_hash source_size destination_size destination_stat item_ok=1 + source_hash=$(/usr/bin/sha256sum "$source_file" 2>/dev/null | /usr/bin/cut -d' ' -f1) + destination_hash=$(/usr/bin/sha256sum "$destination" 2>/dev/null | /usr/bin/cut -d' ' -f1) + source_size=$(/usr/bin/stat -c '%s' "$source_file" 2>/dev/null) + destination_size=$(/usr/bin/stat -c '%s' "$destination" 2>/dev/null) + destination_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$destination" 2>/dev/null) + [ -n "$source_hash" ] && [ "$destination_hash" = "$source_hash" ] || item_ok=0 + [ -n "$source_size" ] && [ "$destination_size" = "$source_size" ] || item_ok=0 + [ "$destination_stat" = "$uid:$gid:${mode#0}:regular file" ] || item_ok=0 + if [ "$item_ok" -eq 1 ]; then + pass "$destination verified" + else + fail "$destination hash, size, or metadata mismatch" + fi + [ "$item_ok" -eq 1 ] +} + +atomic_install() { + local source_file=$1 destination=$2 uid=$3 gid=$4 mode=$5 + local temporary=${destination}.phase2b-new install_ok=1 source_hash source_size + if [ -e "$destination" ] || [ -L "$destination" ] || [ -e "$temporary" ] || [ -L "$temporary" ]; then + fail "$destination or its temporary path exists" + return 1 + fi + source_hash=$(/usr/bin/sha256sum "$source_file" | /usr/bin/cut -d' ' -f1) + source_size=$(/usr/bin/stat -c '%s' "$source_file") + stage1_records+=("FILE_PENDING|$destination|$source_hash|$source_size|$uid|$gid|$mode") + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || install_ok=0 + if [ "$install_ok" -eq 1 ]; then /usr/bin/install -o "$uid" -g "$gid" -m "$mode" -T "$source_file" "$temporary" || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then verify_file "$source_file" "$temporary" "$uid" "$gid" "$mode" || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then { /usr/bin/sync -f "$temporary" 2>/dev/null || /usr/bin/sync; } || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then /usr/bin/ln -- "$temporary" "$destination" || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then { /usr/bin/sync -f "$destination" 2>/dev/null || /usr/bin/sync; } || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then { /usr/bin/sync -f "$(dirname -- "$destination")" 2>/dev/null || /usr/bin/sync; } || install_ok=0; fi + if [ -e "$temporary" ] && ! /usr/bin/rm -- "$temporary"; then install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then verify_file "$source_file" "$destination" "$uid" "$gid" "$mode" || install_ok=0; fi + if [ "$install_ok" -eq 1 ]; then + stage1_records[${#stage1_records[@]}-1]="FILE|$destination|$source_hash|$source_size|$uid|$gid|$mode" + installed_count=$((installed_count + 1)) + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || install_ok=0 + fi + if [ "$install_ok" -ne 1 ]; then fail "$destination installation or state publication"; fi + [ "$install_ok" -eq 1 ] +} + +if [ "$(id -u)" = 0 ]; then pass 'running as root'; else fail 'must run as root'; fi +source_commit=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" rev-parse HEAD 2>/dev/null) +source_commit_rc=$? +source_status=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" status --porcelain 2>/dev/null) +source_status_rc=$? +if [ "$source_commit_rc" -eq 0 ] && [[ "$source_commit" =~ ^[0-9a-f]{40}$ ]]; then pass "source commit $source_commit"; else fail 'source commit unavailable'; fi +if [ "$source_status_rc" -eq 0 ] && [ -z "$source_status" ]; then pass 'source tree clean'; else fail 'source tree is dirty or unreadable'; fi +if (cd "$artifact_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'artifact manifest'; else fail 'artifact manifest'; fi +if (cd "$artifact_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS); then pass 'root inventory'; else fail 'root inventory'; fi + +if [ "$failures" -eq 0 ]; then + /bin/sh "$artifact_root/tests/00-root-preinstall-validate.sh" + validator_rc=$? + if [ "$validator_rc" -eq 0 ]; then pass 'root pre-install validator'; else fail "root pre-install validator rc=$validator_rc"; fi +fi + +safe_shared_directory /etc || true +safe_shared_directory /etc/systemd/system || true +safe_shared_directory /usr/local || true +safe_shared_directory /var || true +safe_shared_directory /var/lib || true +if [ ! -d /srv/le-app-codex ] || [ -L /srv/le-app-codex ]; then fail '/srv/le-app-codex boundary'; fi +if [ ! -d /srv/le-app-codex/home ] || [ -L /srv/le-app-codex/home ]; then fail '/srv/le-app-codex/home boundary'; fi + +expect_absent_file "$state_current" +expect_absent_file /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED + +destinations=( + /usr/local/libexec/dockerd-rootless-29.6.1.sh + /usr/local/libexec/le-app-codex-netns + /etc/le-app-codex-runtime/resolv.conf + /etc/nftables.d/50-le-app-codex.nft + /etc/systemd/system/le-app-codex-netns.service + /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + /srv/le-app-codex/home/.config/systemd/user/docker.service + /srv/le-app-codex/home/.config/docker/daemon.json + /srv/le-app-codex/home/.docker/config.json + "$context_destination" + /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh + /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh + /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh + /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh +) +sources=( + "$payload_root/usr/local/libexec/dockerd-rootless-29.6.1.sh" "$payload_root/usr/local/libexec/le-app-codex-netns" "$payload_root/etc/le-app-codex-runtime/resolv.conf" "$payload_root/etc/nftables.d/50-le-app-codex.nft" + "$payload_root/etc/systemd/system/le-app-codex-netns.service" "$payload_root/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf" "$payload_root/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf" + "$payload_root/srv/le-app-codex/home/.config/systemd/user/docker.service" "$payload_root/srv/le-app-codex/home/.config/docker/daemon.json" "$payload_root/srv/le-app-codex/home/.docker/config.json" "$context_source" + "$artifact_root/tests/00-read-only-preflight.sh" "$artifact_root/tests/00-root-preinstall-validate.sh" "$artifact_root/tests/10-inert-containment.sh" "$artifact_root/tests/20-rootless-docker.sh" "$artifact_root/tests/run-inert-without-user-manager.sh" +) +uids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +gids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +modes=(0755 0755 0644 0644 0644 0644 0644 0640 0640 0640 0640 0750 0750 0750 0750 0750) +allowed_directories=( + /srv/le-app-codex/home/.docker/contexts/meta/$context_hash /srv/le-app-codex/home/.docker/contexts/meta /srv/le-app-codex/home/.docker/contexts /srv/le-app-codex/home/.docker + /srv/le-app-codex/home/.config/systemd/user /srv/le-app-codex/home/.config/systemd /srv/le-app-codex/home/.config/docker /srv/le-app-codex/home/.config /srv/le-app-codex/phase2b-tests + /etc/systemd/system/user@1200.service.d /etc/systemd/system/user-1200.slice.d /etc/le-app-codex-runtime /etc/nftables.d /usr/local/libexec "$state_dir" + /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc +) +directory_uids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 0 0 0 0) +directory_gids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 1200 0 0 0) +directory_modes=(0750 0750 0750 0750 0750 0750 0750 0750 0750 0755 0755 0755 0755 0755 0700 0700 0750 0755 0755 0755) +directory_kinds=(project project project project project project project project project project project project shared shared state project-boundary project-boundary shared-base shared-base shared-base) +declare -A stage1_allowed_file_specs=() stage1_allowed_directory_specs=() stage1_never_created_directories=() +for index in "${!destinations[@]}"; do source_hash=$(/usr/bin/sha256sum "${sources[$index]}" 2>/dev/null | /usr/bin/cut -d' ' -f1); source_size=$(/usr/bin/stat -c '%s' "${sources[$index]}" 2>/dev/null); stage1_allowed_file_specs["${destinations[$index]}"]="$source_hash|$source_size|${uids[$index]}|${gids[$index]}|${modes[$index]}"; done +for index in "${!allowed_directories[@]}"; do stage1_allowed_directory_specs["${allowed_directories[$index]}"]="${directory_uids[$index]}|${directory_gids[$index]}|${directory_modes[$index]}|${directory_kinds[$index]}"; done +for directory in /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc; do stage1_never_created_directories["$directory"]=1; done +for destination in "${destinations[@]}"; do expect_absent_file "$destination"; done + +state_parent_preexisting=0 +if [ -e "$state_dir" ] || [ -L "$state_dir" ]; then + state_parent_preexisting=1 + state_parent_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$state_dir" 2>/dev/null) + if [ -L "$state_dir" ] || [ "$state_parent_stat" != '0:0:700:directory' ]; then fail "$state_dir must be a real root:root mode 0700 directory"; fi + if ! state_existing_content=$(/usr/bin/find "$state_dir" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null); then fail "$state_dir inspection"; fi + if [ -n "$state_existing_content" ]; then fail "$state_dir is not empty; recover or review existing state before retry"; fi +fi +if [ "$failures" -eq 0 ] && [ "$state_parent_preexisting" -eq 0 ]; then /usr/bin/install -d -o 0 -g 0 -m 0700 "$state_dir" || fail "$state_dir creation"; fi +if [ "$failures" -eq 0 ]; then /usr/bin/install -d -o 0 -g 0 -m 0700 "$state_transaction" || fail "$state_transaction creation"; fi +if [ "$failures" -eq 0 ]; then + umask 077 + stage1_source_commit=$source_commit + stage1_artifact_digest=$(/usr/bin/sha256sum "$artifact_root/MANIFEST.sha256" | /usr/bin/cut -d' ' -f1) + stage1_timestamp=$(/usr/bin/date --iso-8601=seconds) + stage1_hostname=$(/usr/bin/hostname) + if [ "$state_parent_preexisting" -eq 1 ]; then stage1_records+=("DIR_EXISTING|$state_dir|0|0|0700|state"); else stage1_records+=("DIR_CREATED|$state_dir|0|0|0700|state"); fi + stage1_state_publish IN_PROGRESS "$state_transaction" 0 0 0600 || fail 'publish initial IN_PROGRESS state' +fi +if [ "$failures" -eq 0 ]; then record_existing_base /etc shared-base; fi +if [ "$failures" -eq 0 ]; then record_existing_base /etc/systemd/system shared-base; fi +if [ "$failures" -eq 0 ]; then record_existing_base /usr/local shared-base; fi +if [ "$failures" -eq 0 ]; then record_existing_base /srv/le-app-codex project-boundary; fi +if [ "$failures" -eq 0 ]; then record_existing_base /srv/le-app-codex/home project-boundary; fi + +if [ "$failures" -eq 0 ]; then exact_directory /usr/local/libexec 0 0 0755 shared || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /etc/nftables.d 0 0 0755 shared || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /etc/le-app-codex-runtime 0 0 0755 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /etc/systemd/system/user-1200.slice.d 0 0 0755 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /etc/systemd/system/user@1200.service.d 0 0 0755 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.config 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.config/systemd 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.config/systemd/user 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.config/docker 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.docker 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.docker/contexts 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/home/.docker/contexts/meta 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory "/srv/le-app-codex/home/.docker/contexts/meta/$context_hash" 1200 1200 0750 project || true; fi +if [ "$failures" -eq 0 ]; then exact_directory /srv/le-app-codex/phase2b-tests 1200 1200 0750 project || true; fi + +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/usr/local/libexec/dockerd-rootless-29.6.1.sh" /usr/local/libexec/dockerd-rootless-29.6.1.sh 0 0 0755 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/usr/local/libexec/le-app-codex-netns" /usr/local/libexec/le-app-codex-netns 0 0 0755 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/etc/le-app-codex-runtime/resolv.conf" /etc/le-app-codex-runtime/resolv.conf 0 0 0644 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/etc/nftables.d/50-le-app-codex.nft" /etc/nftables.d/50-le-app-codex.nft 0 0 0644 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/etc/systemd/system/le-app-codex-netns.service" /etc/systemd/system/le-app-codex-netns.service 0 0 0644 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf" /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf 0 0 0644 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf" /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf 0 0 0644 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/srv/le-app-codex/home/.config/systemd/user/docker.service" /srv/le-app-codex/home/.config/systemd/user/docker.service 1200 1200 0640 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/srv/le-app-codex/home/.config/docker/daemon.json" /srv/le-app-codex/home/.config/docker/daemon.json 1200 1200 0640 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$payload_root/srv/le-app-codex/home/.docker/config.json" /srv/le-app-codex/home/.docker/config.json 1200 1200 0640 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$context_source" "$context_destination" 1200 1200 0640 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$artifact_root/tests/00-read-only-preflight.sh" /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh 1200 1200 0750 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$artifact_root/tests/00-root-preinstall-validate.sh" /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh 1200 1200 0750 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$artifact_root/tests/10-inert-containment.sh" /srv/le-app-codex/phase2b-tests/10-inert-containment.sh 1200 1200 0750 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$artifact_root/tests/20-rootless-docker.sh" /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh 1200 1200 0750 || true; fi +if [ "$failures" -eq 0 ]; then atomic_install "$artifact_root/tests/run-inert-without-user-manager.sh" /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh 1200 1200 0750 || true; fi + +if [ "$failures" -eq 0 ] && [ "$installed_count" -eq 16 ]; then stage1_state_publish COMPLETE "$state_transaction" 0 0 0600 || fail 'publish final COMPLETE state'; fi + +if [ ! -e /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED ] && [ ! -L /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED ]; then pass 'approval marker absent'; else fail 'approval marker exists'; fi +manager_state=$(/usr/bin/systemctl show user@1200.service -p ActiveState --value 2>/dev/null) +if [ "$manager_state" = inactive ]; then pass 'user manager inactive'; else fail "user manager state ${manager_state:-unknown}"; fi +if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -L /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ] && [ ! -L /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi +uid_processes=$(/usr/bin/pgrep -u 1200 2>/dev/null) +if [ -z "$uid_processes" ]; then pass 'UID 1200 processes absent'; else fail 'UID 1200 processes present'; fi +if [ ! -e /run/user/1200/docker.sock ] && [ ! -L /run/user/1200/docker.sock ]; then pass 'Docker socket absent'; else fail 'Docker socket path present'; fi +namespace=$(/usr/bin/ip netns list 2>/dev/null | /usr/bin/awk '$1 == "le-app-codex" { print $1 }') +if [ -z "$namespace" ] && [ ! -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ]; then pass 'namespace absent'; else fail 'namespace present'; fi +if /usr/bin/nft list table inet le_app_codex >/dev/null 2>&1; then fail 'inet policy loaded'; else pass 'inet policy absent'; fi +if /usr/bin/nft list table ip le_app_codex_nat >/dev/null 2>&1; then fail 'NAT policy loaded'; else pass 'NAT policy absent'; fi + +printf 'Phase 2B Stage 1 installed=%s state=%s failures=%s\n' "$installed_count" "$state_current" "$failures" +if [ "$failures" -ne 0 ] && [ -f "$state_current" ]; then printf 'Rollback: /bin/bash %s/tests/31-rollback-stage1.sh\n' "$artifact_root" >&2; fi +if [ "$failures" -eq 0 ] && [ "$installed_count" -eq 16 ]; then stage1_state_resolve_current "$state_transaction" 0 0 0600 || failures=$((failures + 1)); fi +current_status=$(/usr/bin/awk -F'|' '$1 == "STATUS" { print $2 }' "${stage1_current_manifest:-/nonexistent}" 2>/dev/null) +[ "$failures" -eq 0 ] && [ "$installed_count" -eq 16 ] && [ "$current_status" = COMPLETE ] diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/31-rollback-stage1.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/31-rollback-stage1.sh new file mode 100644 index 0000000..44eb624 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/31-rollback-stage1.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash + +failures=0 +removed_count=0 +artifact_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +repo_root=$(CDPATH= cd -- "$artifact_root/../../.." 2>/dev/null && pwd) +payload_root=$artifact_root/payload +state_dir=/var/lib/le-app-codex-phase2b +state_transaction=/var/lib/le-app-codex-phase2b/stage1 +context_hash=1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535 +context_source=$payload_root/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +context_destination=/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +source "$artifact_root/tests/stage1-state-lib.sh" +cleanup_resume=0 + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } + +validate_state_tree() { + local root=$1 entry name generation child child_name child_count generation_kind pointer_temp_value + local top_current=0 top_generations=0 checksum_record expected_checksum_record checksum_lines + while IFS= read -r -d '' entry; do + name=${entry##*/} + case "$name" in + current) + stage1_state_verify_file "$entry" 0 0 0600 || fail "unsafe state pointer $entry" + top_current=$((top_current + 1)) + ;; + generations) + stage1_state_path_is_real_directory "$entry" && [ "$(/usr/bin/stat -c '%u:%g:%a' "$entry" 2>/dev/null)" = '0:0:700' ] || fail "unsafe generations directory $entry" + top_generations=$((top_generations + 1)) + ;; + .current.new.[0-9]*) + [[ "$name" =~ ^[.]current[.]new[.][0-9]+$ ]] || fail "invalid state pointer temporary $entry" + stage1_state_verify_file "$entry" 0 0 0600 || fail "unsafe state pointer temporary $entry" + pointer_temp_value=$(<"$entry") + [[ "$pointer_temp_value" =~ ^gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+$ ]] || fail "invalid state pointer temporary content $entry" + ;; + *) fail "unexpected state transaction entry $entry" ;; + esac + done < <(/usr/bin/find "$root" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$top_current" -eq 1 ] || fail 'state transaction must contain exactly one current pointer' + [ "$top_generations" -eq 1 ] || fail 'state transaction must contain exactly one generations directory' + while IFS= read -r -d '' generation; do + name=${generation##*/} + generation_kind= + if [[ "$name" =~ ^gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+$ ]]; then generation_kind=published; fi + if [[ "$name" =~ ^[.]gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+[.]new$ ]]; then generation_kind=incomplete; fi + [ -n "$generation_kind" ] || fail "invalid generation name $name" + stage1_state_path_is_real_directory "$generation" && [ "$(/usr/bin/stat -c '%u:%g:%a' "$generation" 2>/dev/null)" = '0:0:700' ] || fail "unsafe generation directory $generation" + child_count=0 + while IFS= read -r -d '' child; do + child_name=${child##*/} + if [ "$child_name" != manifest ] && [ "$child_name" != manifest.sha256 ]; then fail "unexpected generation content $child"; fi + stage1_state_verify_file "$child" 0 0 0600 || fail "unsafe generation content $child" + child_count=$((child_count + 1)) + done < <(/usr/bin/find "$generation" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + if [ "$generation_kind" = published ] && { [ "$cleanup_resume" -eq 0 ] || [ "$name" = "$stage1_current_generation" ]; }; then [ "$child_count" -eq 2 ] || fail "published generation is incomplete $generation"; fi + if [ -f "$generation/manifest.sha256" ]; then + [ -f "$generation/manifest" ] || fail "generation checksum lacks manifest $generation" + checksum_lines=$(/usr/bin/awk 'END { print NR+0 }' "$generation/manifest.sha256" 2>/dev/null) + checksum_record=$(<"$generation/manifest.sha256") + expected_checksum_record="$(/usr/bin/sha256sum "$generation/manifest" | /usr/bin/cut -d' ' -f1) manifest" + if [ "$checksum_lines" -ne 1 ] || [ "$checksum_record" != "$expected_checksum_record" ] || ! (cd "$generation" && /usr/bin/sha256sum -c manifest.sha256 >/dev/null 2>&1); then fail "generation checksum mismatch $generation"; fi + fi + done < <(/usr/bin/find "$root/generations" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$failures" -eq 0 ] +} + +sources=( + "$payload_root/usr/local/libexec/dockerd-rootless-29.6.1.sh" + "$payload_root/usr/local/libexec/le-app-codex-netns" + "$payload_root/etc/le-app-codex-runtime/resolv.conf" + "$payload_root/etc/nftables.d/50-le-app-codex.nft" + "$payload_root/etc/systemd/system/le-app-codex-netns.service" + "$payload_root/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf" + "$payload_root/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf" + "$payload_root/srv/le-app-codex/home/.config/systemd/user/docker.service" + "$payload_root/srv/le-app-codex/home/.config/docker/daemon.json" + "$payload_root/srv/le-app-codex/home/.docker/config.json" + "$context_source" + "$artifact_root/tests/00-read-only-preflight.sh" + "$artifact_root/tests/00-root-preinstall-validate.sh" + "$artifact_root/tests/10-inert-containment.sh" + "$artifact_root/tests/20-rootless-docker.sh" + "$artifact_root/tests/run-inert-without-user-manager.sh" +) +destinations=( + /usr/local/libexec/dockerd-rootless-29.6.1.sh + /usr/local/libexec/le-app-codex-netns + /etc/le-app-codex-runtime/resolv.conf + /etc/nftables.d/50-le-app-codex.nft + /etc/systemd/system/le-app-codex-netns.service + /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + /srv/le-app-codex/home/.config/systemd/user/docker.service + /srv/le-app-codex/home/.config/docker/daemon.json + /srv/le-app-codex/home/.docker/config.json + "$context_destination" + /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh + /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh + /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh + /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh +) +uids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +gids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +modes=(0755 0755 0644 0644 0644 0644 0644 0640 0640 0640 0640 0750 0750 0750 0750 0750) + +allowed_directories=( + /srv/le-app-codex/home/.docker/contexts/meta/$context_hash + /srv/le-app-codex/home/.docker/contexts/meta + /srv/le-app-codex/home/.docker/contexts + /srv/le-app-codex/home/.docker + /srv/le-app-codex/home/.config/systemd/user + /srv/le-app-codex/home/.config/systemd + /srv/le-app-codex/home/.config/docker + /srv/le-app-codex/home/.config + /srv/le-app-codex/phase2b-tests + /etc/systemd/system/user@1200.service.d + /etc/systemd/system/user-1200.slice.d + /etc/le-app-codex-runtime + /etc/nftables.d + /usr/local/libexec + "$state_dir" + /srv/le-app-codex/home + /srv/le-app-codex + /usr/local + /etc/systemd/system + /etc +) +directory_uids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 0 0 0 0) +directory_gids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 1200 0 0 0) +directory_modes=(0750 0750 0750 0750 0750 0750 0750 0750 0750 0755 0755 0755 0755 0755 0700 0700 0750 0755 0755 0755) +directory_kinds=(project project project project project project project project project project project project shared shared state project-boundary project-boundary shared-base shared-base shared-base) + +declare -A stage1_allowed_file_specs=() stage1_allowed_directory_specs=() stage1_never_created_directories=() +for index in "${!destinations[@]}"; do + source_hash=$(/usr/bin/sha256sum "${sources[$index]}" 2>/dev/null | /usr/bin/cut -d' ' -f1) + source_size=$(/usr/bin/stat -c '%s' "${sources[$index]}" 2>/dev/null) + stage1_allowed_file_specs["${destinations[$index]}"]="$source_hash|$source_size|${uids[$index]}|${gids[$index]}|${modes[$index]}" +done +for index in "${!allowed_directories[@]}"; do stage1_allowed_directory_specs["${allowed_directories[$index]}"]="${directory_uids[$index]}|${directory_gids[$index]}|${directory_modes[$index]}|${directory_kinds[$index]}"; done +for directory in /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc; do stage1_never_created_directories["$directory"]=1; done + +if [ "$(id -u)" = 0 ]; then pass 'running as root'; else fail 'must run as root'; fi +source_status=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" status --porcelain 2>/dev/null) +source_status_rc=$? +if [ "$source_status_rc" -eq 0 ] && [ -z "$source_status" ]; then pass 'artifact worktree clean'; else fail 'artifact worktree is dirty or unreadable'; fi +if (cd "$artifact_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'artifact manifest'; else fail 'artifact manifest'; fi +current_artifact_digest=$(/usr/bin/sha256sum "$artifact_root/MANIFEST.sha256" 2>/dev/null | /usr/bin/cut -d' ' -f1) +if stage1_state_path_is_real_directory "$state_dir" && [ "$(/usr/bin/stat -c '%u:%g:%a' "$state_dir" 2>/dev/null)" = '0:0:700' ]; then pass 'state parent metadata'; else fail 'state parent must be a real root:root mode 0700 directory'; fi +cleanup_candidates=$(/usr/bin/find "$state_dir" -mindepth 1 -maxdepth 1 -name '.stage1.cleanup.*' -print 2>/dev/null) +cleanup_count=$(printf '%s\n' "$cleanup_candidates" | /usr/bin/sed '/^$/d' | /usr/bin/wc -l) +active_transaction=0 +if [ -e "$state_transaction" ] || [ -L "$state_transaction" ]; then active_transaction=1; stage1_state_path_is_real_directory "$state_transaction" || fail 'active state transaction must be a real directory'; fi +if [ "$active_transaction" -eq 0 ] && [ "$cleanup_count" -eq 1 ]; then + cleanup_name=${cleanup_candidates##*/} + [[ "$cleanup_name" =~ ^[.]stage1[.]cleanup[.]gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+$ ]] || fail 'cleanup state transaction name' + stage1_state_path_is_real_directory "$cleanup_candidates" || fail 'cleanup state transaction must be a real directory' + state_transaction=$cleanup_candidates + cleanup_resume=1 +fi +if [ "$active_transaction" -eq 1 ] && [ "$cleanup_count" -ne 0 ]; then fail 'both active and cleanup state transactions exist'; fi +if [ "$active_transaction" -eq 0 ] && [ "$cleanup_count" -ne 1 ]; then fail 'exactly one active or cleanup state transaction is required'; fi +stage1_state_resolve_current "$state_transaction" 0 0 0600 || fail 'authoritative state generation' +if [ "$failures" -eq 0 ]; then stage1_state_validate_records "$stage1_current_manifest" "$current_artifact_digest" "$(/usr/bin/hostname)" 16 || fail 'authoritative state records'; fi +if [ "$failures" -eq 0 ]; then validate_state_tree "$state_transaction" || true; fi + +if [ ! -e /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED ] && [ ! -L /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED ]; then pass 'approval marker absent'; else fail 'approval marker exists'; fi +manager_state=$(/usr/bin/systemctl show user@1200.service -p ActiveState --value 2>/dev/null) +if [ "$manager_state" = inactive ]; then pass 'user manager inactive'; else fail 'user manager active or unknown'; fi +if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -L /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ] && [ ! -L /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi +uid_processes=$(/usr/bin/pgrep -u 1200 2>/dev/null) +if [ -z "$uid_processes" ]; then pass 'UID 1200 processes absent'; else fail 'UID 1200 processes present'; fi +if [ ! -e /run/user/1200/docker.sock ] && [ ! -L /run/user/1200/docker.sock ]; then pass 'Docker socket absent'; else fail 'Docker socket path present'; fi +namespace=$(/usr/bin/ip netns list 2>/dev/null | /usr/bin/awk '$1 == "le-app-codex" { print $1 }') +if [ -z "$namespace" ] && [ ! -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ]; then pass 'namespace absent'; else fail 'namespace present'; fi +if /usr/bin/nft list table inet le_app_codex >/dev/null 2>&1; then fail 'inet policy loaded'; else pass 'inet policy absent'; fi +if /usr/bin/nft list table ip le_app_codex_nat >/dev/null 2>&1; then fail 'NAT policy loaded'; else pass 'NAT policy absent'; fi + +declare -A remove_files=() created_directories=() +for path in "${stage1_validated_file_paths[@]}" "${stage1_validated_pending_paths[@]}"; do + [ -n "$path" ] || continue + spec=${stage1_allowed_file_specs[$path]} + IFS='|' read -r expected_hash expected_size expected_uid expected_gid expected_mode <<< "$spec" + temporary=${path}.phase2b-new + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + if [ "$cleanup_resume" -eq 1 ]; then + pass "$path already removed before rollback retry" + elif [[ " ${stage1_validated_pending_paths[*]} " = *" $path "* ]]; then + pass "$path pending and absent" + else + fail "$path recorded complete but absent" + fi + else + actual_hash=$(/usr/bin/sha256sum "$path" 2>/dev/null | /usr/bin/cut -d' ' -f1) + actual_size=$(/usr/bin/stat -c '%s' "$path" 2>/dev/null) + actual_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + if [ "$actual_hash" = "$expected_hash" ] && [ "$actual_size" = "$expected_size" ] && [ "$actual_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:regular file" ]; then remove_files["$path"]=1; pass "$path verified"; else fail "$path differs from committed artifact"; fi + fi + if [ -e "$temporary" ] || [ -L "$temporary" ]; then + temp_hash=$(/usr/bin/sha256sum "$temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1) + temp_size=$(/usr/bin/stat -c '%s' "$temporary" 2>/dev/null) + temp_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$temporary" 2>/dev/null) + if [ "$temp_hash" = "$expected_hash" ] && [ "$temp_size" = "$expected_size" ] && [ "$temp_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:regular file" ]; then remove_files["$temporary"]=1; else fail "$temporary differs from pending committed artifact"; fi + fi +done +for directory in "${stage1_validated_created_directories[@]}"; do + [ -n "$directory" ] || continue + spec=${stage1_allowed_directory_specs[$directory]} + IFS='|' read -r expected_uid expected_gid expected_mode expected_kind <<< "$spec" + if [ ! -e "$directory" ] && [ ! -L "$directory" ]; then + if [ "$cleanup_resume" -eq 1 ]; then pass "$directory already removed before rollback retry"; else fail "$directory recorded created but absent"; fi + else + actual_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$directory" 2>/dev/null) + if [ ! -L "$directory" ] && [ "$actual_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:directory" ]; then created_directories["$directory"]=1; else fail "$directory created metadata mismatch"; fi + fi +done +for directory in "${stage1_validated_pending_directories[@]}"; do + [ -n "$directory" ] || continue + spec=${stage1_allowed_directory_specs[$directory]} + IFS='|' read -r expected_uid expected_gid expected_mode expected_kind <<< "$spec" + if [ ! -e "$directory" ] && [ ! -L "$directory" ]; then + pass "$directory pending and absent" + else + actual_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$directory" 2>/dev/null) + if [ ! -L "$directory" ] && [ "$actual_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:directory" ]; then created_directories["$directory"]=1; else fail "$directory pending metadata mismatch"; fi + fi +done + +if [ "$failures" -eq 0 ] && [ "$cleanup_resume" -eq 0 ]; then + tombstone=$state_dir/.stage1.cleanup.$stage1_current_generation + [ ! -e "$tombstone" ] && [ ! -L "$tombstone" ] || fail 'cleanup tombstone already exists' + if [ "$failures" -eq 0 ]; then /usr/bin/mv -T -- "$state_transaction" "$tombstone" || fail 'atomic rollback transition'; fi + if [ "$failures" -eq 0 ]; then { /usr/bin/sync -f "$state_dir" 2>/dev/null || /usr/bin/sync; } || fail 'rollback transition sync'; fi + if [ "$failures" -eq 0 ]; then state_transaction=$tombstone; cleanup_resume=1; fi +fi + +if [ "$failures" -eq 0 ]; then + for destination in "${destinations[@]}"; do + temporary=${destination}.phase2b-new + if [ "${remove_files[$temporary]:-0}" -eq 1 ]; then /usr/bin/rm -- "$temporary" || fail "$temporary removal"; fi + if [ "${remove_files[$destination]:-0}" -eq 1 ]; then /usr/bin/rm -- "$destination" && removed_count=$((removed_count + 1)) || fail "$destination removal"; fi + done +fi +if [ "$failures" -eq 0 ]; then + for directory in "${allowed_directories[@]}"; do + if [ "$directory" != "$state_dir" ] && [ "${created_directories[$directory]:-0}" -eq 1 ]; then /usr/bin/rmdir -- "$directory" || fail "$directory removal"; fi + done +fi + +if [ "$failures" -eq 0 ]; then + generations=$state_transaction/generations + current_id=$stage1_current_generation + while IFS= read -r -d '' entry; do + generation_name=${entry##*/} + if [ "$generation_name" != "$current_id" ]; then + if [ -e "$entry/manifest.sha256" ] && ! /usr/bin/rm -- "$entry/manifest.sha256"; then fail "$entry checksum removal"; fi + if [ -e "$entry/manifest" ] && ! /usr/bin/rm -- "$entry/manifest"; then fail "$entry manifest removal"; fi + if [ "$failures" -eq 0 ]; then /usr/bin/rmdir -- "$entry" || fail "$entry removal"; fi + fi + done < <(/usr/bin/find "$state_transaction/generations" -mindepth 1 -maxdepth 1 -print0) +fi +if [ "$failures" -eq 0 ]; then + while IFS= read -r -d '' entry; do + name=${entry##*/} + if [[ "$name" =~ ^[.]current[.]new[.][0-9]+$ ]]; then /usr/bin/rm -- "$entry" || fail "$entry removal"; fi + done < <(/usr/bin/find "$state_transaction" -mindepth 1 -maxdepth 1 -name '.current.new.*' -print0) +fi +if [ "$failures" -eq 0 ]; then + /usr/bin/rm -- "$state_transaction/current" || fail 'current pointer removal' + if [ "$failures" -eq 0 ]; then /usr/bin/rm -- "$state_transaction/generations/$current_id/manifest.sha256" || fail 'authoritative checksum removal'; fi + if [ "$failures" -eq 0 ]; then /usr/bin/rm -- "$state_transaction/generations/$current_id/manifest" || fail 'authoritative manifest removal'; fi + if [ "$failures" -eq 0 ]; then /usr/bin/rmdir -- "$state_transaction/generations/$current_id" || fail 'authoritative generation removal'; fi + if [ "$failures" -eq 0 ]; then /usr/bin/rmdir -- "$state_transaction/generations" || fail 'generations directory removal'; fi + if [ "$failures" -eq 0 ]; then /usr/bin/rmdir -- "$state_transaction" || fail 'state transaction removal'; fi +fi +if [ "$failures" -eq 0 ] && [ "${created_directories[$state_dir]:-0}" -eq 1 ]; then /usr/bin/rmdir -- "$state_dir" || fail 'state directory removal'; fi +if [ "$failures" -eq 0 ] && [ "${created_directories[$state_dir]:-0}" -eq 0 ]; then pass 'pre-existing state directory preserved'; fi + +printf 'Phase 2B Stage 1 rollback status=%s removed=%s failures=%s\n' "${stage1_validated_status:-UNKNOWN}" "$removed_count" "$failures" +[ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/32-stage1-state-tests.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/32-stage1-state-tests.sh new file mode 100644 index 0000000..aecc4ec --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/32-stage1-state-tests.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash + +failures=0 +tests=0 +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +source "$root/tests/stage1-state-lib.sh" +test_root=$(/usr/bin/mktemp -d /tmp/le-phase2b-state-test.XXXXXX) +uid=$(id -u) +gid=$(id -g) + +ok() { tests=$((tests + 1)); printf 'PASS: %s\n' "$1"; } +bad() { failures=$((failures + 1)); printf 'FAIL: %s\n' "$1" >&2; } +assert() { local label=$1; shift; if "$@"; then ok "$label"; else bad "$label"; fi; } +reject() { local label=$1; shift; if "$@"; then bad "$label"; else ok "$label"; fi; } + +new_case() { + local name=$1 index file hash size + case_dir=$test_root/$name + state_root=$case_dir/state + /usr/bin/install -d -m 0700 "$state_root" "$case_dir/fs" + stage1_source_commit=1111111111111111111111111111111111111111 + stage1_artifact_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + stage1_timestamp=2026-07-12T00:00:00-04:00 + stage1_hostname=TITANSERVER + stage1_generation_counter=0 + stage1_records=("DIR_EXISTING|$state_root|$uid|$gid|0700|state") + declare -gA stage1_allowed_file_specs=() stage1_allowed_directory_specs=() stage1_never_created_directories=() + stage1_allowed_directory_specs["$state_root"]="$uid|$gid|0700|state" + for index in $(/usr/bin/seq 1 16); do + file=$case_dir/fs/file$index + printf 'file-%s\n' "$index" > "$file.source" + hash=$(/usr/bin/sha256sum "$file.source" | /usr/bin/cut -d' ' -f1) + size=$(/usr/bin/stat -c '%s' "$file.source") + stage1_allowed_file_specs["$file"]="$hash|$size|$uid|$gid|0600" + done + unset PHASE2B_FAILPOINT +} + +validate_current() { + stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 || return 1 + stage1_state_validate_records "$stage1_current_manifest" "$stage1_artifact_digest" "$stage1_hostname" 16 +} + +add_file_record() { + local index=$1 type=${2:-FILE} path spec + path=$case_dir/fs/file$index + spec=${stage1_allowed_file_specs[$path]} + stage1_records+=("$type|$path|$spec") +} + +validate_crafted_records() { + local crafted=$case_dir/crafted.manifest + { + printf 'FORMAT|le-app-codex-phase2b-stage1|3\n' + printf 'STATUS|IN_PROGRESS\n' + printf 'SOURCE_COMMIT|%s\n' "$stage1_source_commit" + printf 'ARTIFACT_MANIFEST_SHA256|%s\n' "$stage1_artifact_digest" + printf 'TIMESTAMP|%s\n' "$stage1_timestamp" + printf 'HOSTNAME|%s\n' "$stage1_hostname" + if [ "${#stage1_records[@]}" -gt 0 ]; then printf '%s\n' "${stage1_records[@]}"; fi + } > "$crafted" + stage1_state_validate_records "$crafted" "$stage1_artifact_digest" "$stage1_hostname" 16 +} + +simulate_validated_rollback() { + local force_directory_failure=${1:-} path spec hash size + validate_current || return 1 + for path in "${stage1_validated_file_paths[@]}" "${stage1_validated_pending_paths[@]}"; do + [ -n "$path" ] || continue + if [ -e "$path" ]; then + spec=${stage1_allowed_file_specs[$path]} + IFS='|' read -r hash size _ <<< "$spec" + [ "$(/usr/bin/sha256sum "$path" | /usr/bin/cut -d' ' -f1)" = "$hash" ] || return 1 + /usr/bin/rm -- "$path" || return 1 + fi + done + for path in "${stage1_validated_created_directories[@]}" "${stage1_validated_pending_directories[@]}"; do + [ -n "$path" ] || continue + [ "$path" != "$state_root" ] || continue + if [ "$path" = "$force_directory_failure" ]; then return 1; fi + if [ -d "$path" ]; then /usr/bin/rmdir -- "$path" || return 1; fi + done +} + +new_case empty_array +stage1_records=() +assert 'empty record array publishes' stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'empty record array emits no blank line' bash -c '! grep -n "^$" "$1"' _ "$state_root/generations/$(<"$state_root/current")/manifest" +assert 'empty record array passes real rollback record validator' validate_current + +new_case initial +assert 'initial publication with directory record' stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'initial generation resolves and verifies' validate_current +assert 'initial state contains only headers and directory records' bash -c '! grep -Ev "^(FORMAT|STATUS|SOURCE_COMMIT|ARTIFACT_MANIFEST_SHA256|TIMESTAMP|HOSTNAME|DIR_EXISTING)\\|" "$1"' _ "$stage1_current_manifest" + +for point in after_manifest after_checksum before_commit; do + new_case initial_$point + PHASE2B_FAILPOINT=$point + reject "initial interruption $point" stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 + reject "initial interruption $point has no authoritative pointer" stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 + assert "initial interruption $point leaves unreferenced generation data" bash -c 'find "$1/generations" -mindepth 1 -print -quit | grep -q .' _ "$state_root" +done + +new_case initial_after_commit +PHASE2B_FAILPOINT=after_commit +reject 'initial interruption immediately after commit point reports failure' stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'initial post-commit pointer resolves a complete generation' validate_current +assert 'initial post-commit generation remains IN_PROGRESS' test "$stage1_validated_status" = IN_PROGRESS + +new_case existing_update +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +previous_generation=$(<"$state_root/current") +add_file_record 1 FILE +PHASE2B_FAILPOINT=before_commit +reject 'update interruption before atomic pointer switch' stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'previous IN_PROGRESS generation remains authoritative' test "$(<"$state_root/current")" = "$previous_generation" +assert 'previous generation still validates' validate_current + +new_case after_commit +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +previous_generation=$(<"$state_root/current") +add_file_record 1 FILE +PHASE2B_FAILPOINT=after_commit +reject 'interruption immediately after commit point reports failure' stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'after-commit pointer exposes new generation' test "$(<"$state_root/current")" != "$previous_generation" +assert 'after-commit generation is complete and verified' validate_current + +new_case pointer_escape +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +printf '../outside\n' > "$state_root/.bad-current" +/usr/bin/chmod 0600 "$state_root/.bad-current" +/usr/bin/mv -T -- "$state_root/.bad-current" "$state_root/current" +reject 'escaping current pointer target is rejected' stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 + +new_case pointer_symlink +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +pointer_generation=$(<"$state_root/current") +/usr/bin/rm -- "$state_root/current" +/usr/bin/ln -s -- "$pointer_generation" "$state_root/current" +reject 'symlink current pointer is rejected' stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 + +new_case generation_symlink +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +pointer_generation=$(<"$state_root/current") +/usr/bin/mv -- "$state_root/generations/$pointer_generation" "$state_root/generations/${pointer_generation}.real" +/usr/bin/ln -s -- "${pointer_generation}.real" "$state_root/generations/$pointer_generation" +reject 'symlink generation target is rejected' stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 + +new_case generation_extra +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +pointer_generation=$(<"$state_root/current") +printf 'unexpected\n' > "$state_root/generations/$pointer_generation/extra" +reject 'generation with unverified extra content is rejected' stage1_state_resolve_current "$state_root" "$uid" "$gid" 0600 + +new_case complete_before_commit +for index in $(/usr/bin/seq 1 16); do add_file_record "$index" FILE; done +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +previous_generation=$(<"$state_root/current") +PHASE2B_FAILPOINT=before_commit +reject 'final COMPLETE interruption before commit point' stage1_state_publish COMPLETE "$state_root" "$uid" "$gid" 0600 +assert 'final COMPLETE pre-commit interruption preserves IN_PROGRESS generation' test "$(<"$state_root/current")" = "$previous_generation" +assert 'preserved final pre-commit generation validates' validate_current +assert 'preserved final pre-commit generation remains IN_PROGRESS' test "$stage1_validated_status" = IN_PROGRESS + +new_case complete_after_commit +for index in $(/usr/bin/seq 1 16); do add_file_record "$index" FILE; done +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +previous_generation=$(<"$state_root/current") +PHASE2B_FAILPOINT=after_commit +reject 'final COMPLETE interruption immediately after commit point' stage1_state_publish COMPLETE "$state_root" "$uid" "$gid" 0600 +assert 'final COMPLETE post-commit interruption switches generation' test "$(<"$state_root/current")" != "$previous_generation" +assert 'final COMPLETE post-commit generation validates' validate_current +assert 'final COMPLETE post-commit status is COMPLETE' test "$stage1_validated_status" = COMPLETE + +new_case complete +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +for index in $(/usr/bin/seq 1 16); do add_file_record "$index" FILE; done +assert 'transition IN_PROGRESS to COMPLETE' stage1_state_publish COMPLETE "$state_root" "$uid" "$gid" 0600 +assert 'COMPLETE validates with exactly 16 files' validate_current +assert 'real validator reports COMPLETE' test "$stage1_validated_status" = COMPLETE +for index in $(/usr/bin/seq 1 16); do cp "$case_dir/fs/file$index.source" "$case_dir/fs/file$index"; done +assert 'rollback simulation from COMPLETE uses real validator' simulate_validated_rollback + +new_case partial +for index in 1 2 3; do add_file_record "$index" FILE; cp "$case_dir/fs/file$index.source" "$case_dir/fs/file$index"; done +add_file_record 4 FILE_PENDING +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +assert 'IN_PROGRESS subset validates' validate_current +assert 'real validator exposes several completed files' test "${#stage1_validated_file_paths[@]}" -eq 3 +assert 'real validator exposes pending subset' test "${#stage1_validated_pending_paths[@]}" -eq 1 +assert 'rollback simulation from IN_PROGRESS uses real validator' simulate_validated_rollback + +new_case directory_failure +created_dir=$case_dir/fs/created +/usr/bin/mkdir "$created_dir" +printf 'blocker\n' > "$created_dir/blocker" +stage1_allowed_directory_specs["$created_dir"]="$uid|$gid|0755|project" +stage1_records+=("DIR_CREATED|$created_dir|$uid|$gid|0755|project") +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +reject 'directory-removal failure is detected' simulate_validated_rollback "$created_dir" +assert 'directory-removal failure preserves authoritative current pointer' test -f "$state_root/current" +assert 'directory-removal failure preserves verified generation' validate_current + +new_case malformed_blank +stage1_records+=("") +reject 'blank record rejected by shared validator' validate_crafted_records + +new_case malformed_duplicate +add_file_record 1 FILE +add_file_record 1 FILE +reject 'duplicate file record rejected by shared validator' validate_crafted_records + +new_case malformed_unknown +stage1_records+=("UNKNOWN|value") +reject 'unknown record rejected by shared validator' validate_crafted_records + +new_case malformed_unapproved_file +stage1_records+=("FILE|$case_dir/fs/not-approved|deadbeef|1|$uid|$gid|0600") +reject 'unapproved file record rejected by shared validator' validate_crafted_records + +new_case malformed_escape +escape_file=$case_dir/fs/../escape +stage1_allowed_file_specs["$escape_file"]="deadbeef|1|$uid|$gid|0600" +stage1_records+=("FILE|$escape_file|deadbeef|1|$uid|$gid|0600") +reject 'escaping path record rejected by shared validator' validate_crafted_records + +new_case malformed_trailing_delimiter +add_file_record 1 FILE +stage1_records[${#stage1_records[@]}-1]="${stage1_records[${#stage1_records[@]}-1]}|" +reject 'trailing record delimiter rejected by shared validator' validate_crafted_records + +new_case malformed_pending_complete +for index in $(/usr/bin/seq 1 15); do add_file_record "$index" FILE; done +add_file_record 16 FILE_PENDING +reject 'COMPLETE with pending record rejected by publication validator' stage1_state_publish COMPLETE "$state_root" "$uid" "$gid" 0600 + +new_case malformed_directory_duplicate +stage1_records+=("DIR_EXISTING|$state_root|$uid|$gid|0700|state") +reject 'duplicate directory record rejected by shared validator' validate_crafted_records + +new_case malformed_directory_escape +escape_directory=$case_dir/../escape +stage1_allowed_directory_specs["$escape_directory"]="$uid|$gid|0700|project" +stage1_records+=("DIR_CREATED|$escape_directory|$uid|$gid|0700|project") +reject 'unapproved or escaping directory rejected by shared validator' validate_crafted_records + +new_case malformed_unapproved_directory +stage1_records+=("DIR_CREATED|$case_dir/fs/not-approved|$uid|$gid|0700|project") +reject 'unapproved directory record rejected by shared validator' validate_crafted_records + +new_case manifest_mismatch +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +reject 'mismatched artifact digest refuses validation' stage1_state_validate_records "$stage1_current_manifest" bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb "$stage1_hostname" 16 + +new_case unrelated_commit +stage1_source_commit=2222222222222222222222222222222222222222 +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +git_case=$case_dir/git +/usr/bin/git init -q "$git_case" +printf 'artifact\n' > "$git_case/artifact" +/usr/bin/git -C "$git_case" -c user.name=test -c user.email=test@example.invalid add artifact +/usr/bin/git -C "$git_case" -c user.name=test -c user.email=test@example.invalid commit -q -m artifact +install_commit=$(/usr/bin/git -C "$git_case" rev-parse HEAD) +printf 'unrelated\n' > "$git_case/unrelated" +/usr/bin/git -C "$git_case" -c user.name=test -c user.email=test@example.invalid add unrelated +/usr/bin/git -C "$git_case" -c user.name=test -c user.email=test@example.invalid commit -q -m unrelated +current_unrelated_commit=$(/usr/bin/git -C "$git_case" rev-parse HEAD) +assert 'test repository advanced to unrelated commit' test "$install_commit" != "$current_unrelated_commit" +assert 'later unrelated commit does not block matching artifact validation' validate_current +assert 'recorded installation commit is preserved' test "$stage1_validated_source_commit" = 2222222222222222222222222222222222222222 + +new_case unreferenced +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 +authoritative=$(<"$state_root/current") +add_file_record 1 FILE +PHASE2B_FAILPOINT=after_checksum +stage1_state_publish IN_PROGRESS "$state_root" "$uid" "$gid" 0600 || true +assert 'unreferenced incomplete generation does not replace current' test "$(<"$state_root/current")" = "$authoritative" +assert 'authoritative generation remains valid with unreferenced data present' validate_current + +/usr/bin/rm -rf -- "$test_root" +printf 'Stage 1 state tests=%s failures=%s\n' "$tests" "$failures" +[ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-contained-runtime/tests/stage1-state-lib.sh b/docs/le-app-database-migration/phase2b-contained-runtime/tests/stage1-state-lib.sh new file mode 100644 index 0000000..e26fd74 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-contained-runtime/tests/stage1-state-lib.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash + +stage1_state_path_is_regular_or_absent() { + local path=$1 + [ ! -L "$path" ] || return 1 + [ ! -e "$path" ] || [ -f "$path" ] +} + +stage1_state_path_is_real_directory() { + local path=$1 + [ -d "$path" ] && [ ! -L "$path" ] +} + +stage1_state_verify_file() { + local path=$1 uid=$2 gid=$3 mode=$4 actual + stage1_state_path_is_regular_or_absent "$path" || return 1 + [ -f "$path" ] || return 1 + actual=$(/usr/bin/stat -c '%u:%g:%a' "$path" 2>/dev/null) + [ "$actual" = "$uid:$gid:${mode#0}" ] +} + +stage1_state_resolve_current() { + local root=$1 uid=$2 gid=$3 mode=$4 pointer generations generation_id generation_dir manifest checksum pointer_stat line_count checksum_lines checksum_record expected_checksum_record entry entry_name entry_count=0 + pointer=$root/current + generations=$root/generations + stage1_state_path_is_real_directory "$root" || return 1 + stage1_state_path_is_real_directory "$generations" || return 1 + [ "$(/usr/bin/stat -c '%u:%g:%a' "$root" 2>/dev/null)" = "$uid:$gid:700" ] || return 1 + [ "$(/usr/bin/stat -c '%u:%g:%a' "$generations" 2>/dev/null)" = "$uid:$gid:700" ] || return 1 + stage1_state_verify_file "$pointer" "$uid" "$gid" "$mode" || return 1 + pointer_stat=$(/usr/bin/stat -c '%u:%g:%a' "$pointer" 2>/dev/null) + [ "$pointer_stat" = "$uid:$gid:${mode#0}" ] || return 1 + line_count=$(/usr/bin/awk 'END { print NR+0 }' "$pointer" 2>/dev/null) + [ "$line_count" -eq 1 ] || return 1 + IFS= read -r generation_id < "$pointer" + [[ "$generation_id" =~ ^gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+$ ]] || return 1 + [[ "$generation_id" != */* ]] || return 1 + generation_dir=$generations/$generation_id + stage1_state_path_is_real_directory "$generation_dir" || return 1 + [ "$(/usr/bin/stat -c '%u:%g:%a' "$generation_dir" 2>/dev/null)" = "$uid:$gid:700" ] || return 1 + [ "$(/usr/bin/readlink -f -- "$generation_dir")" = "$(/usr/bin/readlink -f -- "$generations")/$generation_id" ] || return 1 + manifest=$generation_dir/manifest + checksum=$generation_dir/manifest.sha256 + stage1_state_verify_file "$manifest" "$uid" "$gid" "$mode" || return 1 + stage1_state_verify_file "$checksum" "$uid" "$gid" "$mode" || return 1 + while IFS= read -r -d '' entry; do + entry_name=${entry##*/} + { [ "$entry_name" = manifest ] || [ "$entry_name" = manifest.sha256 ]; } || return 1 + [ -f "$entry" ] && [ ! -L "$entry" ] || return 1 + entry_count=$((entry_count + 1)) + done < <(/usr/bin/find "$generation_dir" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$entry_count" -eq 2 ] || return 1 + [ "$(/usr/bin/stat -c '%h' "$pointer" 2>/dev/null)" -eq 1 ] || return 1 + [ "$(/usr/bin/stat -c '%h' "$manifest" 2>/dev/null)" -eq 1 ] || return 1 + [ "$(/usr/bin/stat -c '%h' "$checksum" 2>/dev/null)" -eq 1 ] || return 1 + checksum_lines=$(/usr/bin/awk 'END { print NR+0 }' "$checksum" 2>/dev/null) + checksum_record=$(<"$checksum") + expected_checksum_record="$(/usr/bin/sha256sum "$manifest" | /usr/bin/cut -d' ' -f1) manifest" + [ "$checksum_lines" -eq 1 ] && [ "$checksum_record" = "$expected_checksum_record" ] || return 1 + (cd "$generation_dir" && /usr/bin/sha256sum -c manifest.sha256 >/dev/null 2>&1) || return 1 + stage1_current_generation=$generation_id + stage1_current_generation_dir=$generation_dir + stage1_current_manifest=$manifest + stage1_current_checksum=$checksum +} + +stage1_state_publish() { + local status=$1 root=$2 uid=$3 gid=$4 mode=$5 + local generations generation_id generation_dir generation_tmp manifest checksum pointer pointer_tmp digest publish_ok=1 committed=0 + generations=$root/generations + pointer=$root/current + stage1_state_path_is_real_directory "$root" || return 1 + if [ -e "$generations" ] || [ -L "$generations" ]; then stage1_state_path_is_real_directory "$generations" || return 1; else /usr/bin/install -d -o "$uid" -g "$gid" -m 0700 "$generations" || return 1; { /usr/bin/sync -f "$root" 2>/dev/null || /usr/bin/sync; } || return 1; fi + stage1_state_path_is_regular_or_absent "$pointer" || return 1 + stage1_generation_counter=$((${stage1_generation_counter:-0} + 1)) + generation_id=gen-$(/usr/bin/date -u +%Y%m%dT%H%M%SZ)-$$-$stage1_generation_counter + generation_dir=$generations/$generation_id + generation_tmp=$generations/.$generation_id.new + manifest=$generation_tmp/manifest + checksum=$generation_tmp/manifest.sha256 + pointer_tmp=$root/.current.new.$$ + [ ! -e "$generation_dir" ] && [ ! -L "$generation_dir" ] && [ ! -e "$generation_tmp" ] && [ ! -L "$generation_tmp" ] || return 1 + [ ! -e "$pointer_tmp" ] && [ ! -L "$pointer_tmp" ] || return 1 + /usr/bin/install -d -o "$uid" -g "$gid" -m 0700 "$generation_tmp" || return 1 + { + printf 'FORMAT|le-app-codex-phase2b-stage1|3\n' + printf 'STATUS|%s\n' "$status" + printf 'SOURCE_COMMIT|%s\n' "$stage1_source_commit" + printf 'ARTIFACT_MANIFEST_SHA256|%s\n' "$stage1_artifact_digest" + printf 'TIMESTAMP|%s\n' "$stage1_timestamp" + printf 'HOSTNAME|%s\n' "$stage1_hostname" + if [ "${#stage1_records[@]}" -gt 0 ]; then printf '%s\n' "${stage1_records[@]}"; fi + } > "$manifest" || publish_ok=0 + if [ "$publish_ok" -eq 1 ]; then /usr/bin/chown "$uid:$gid" "$manifest" && /usr/bin/chmod "$mode" "$manifest" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$manifest" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ] && [ "${PHASE2B_FAILPOINT:-}" = after_manifest ]; then publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then digest=$(/usr/bin/sha256sum "$manifest" | /usr/bin/cut -d' ' -f1); printf '%s manifest\n' "$digest" > "$checksum" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then /usr/bin/chown "$uid:$gid" "$checksum" && /usr/bin/chmod "$mode" "$checksum" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$checksum" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then (cd "$generation_tmp" && /usr/bin/sha256sum -c manifest.sha256 >/dev/null 2>&1) || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then declare -p stage1_allowed_file_specs stage1_allowed_directory_specs stage1_never_created_directories >/dev/null 2>&1 || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then stage1_state_validate_records "$manifest" "$stage1_artifact_digest" "$stage1_hostname" 16 || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ] && [ "${PHASE2B_FAILPOINT:-}" = after_checksum ]; then publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$generation_tmp" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then /usr/bin/mv -T -- "$generation_tmp" "$generation_dir" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$generations" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then printf '%s\n' "$generation_id" > "$pointer_tmp" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then /usr/bin/chown "$uid:$gid" "$pointer_tmp" && /usr/bin/chmod "$mode" "$pointer_tmp" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$pointer_tmp" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ] && [ "${PHASE2B_FAILPOINT:-}" = before_commit ]; then publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then /usr/bin/mv -T -- "$pointer_tmp" "$pointer" || publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then committed=1; fi + if [ "$publish_ok" -eq 1 ] && [ "${PHASE2B_FAILPOINT:-}" = after_commit ]; then publish_ok=0; fi + if [ "$publish_ok" -eq 1 ]; then { /usr/bin/sync -f "$root" 2>/dev/null || /usr/bin/sync; } || publish_ok=0; fi + if [ "$committed" -eq 1 ]; then stage1_state_resolve_current "$root" "$uid" "$gid" "$mode" || publish_ok=0; fi + /usr/bin/rm -f -- "$pointer_tmp" + [ "$publish_ok" -eq 1 ] +} + +stage1_state_validate_records() { + local manifest=$1 expected_artifact_digest=$2 expected_hostname=$3 expected_complete_count=$4 + local line number=0 type f1 f2 f3 f4 f5 f6 extra delimiter_text delimiter_count status= source_commit= artifact_digest= timestamp= hostname= + local format_count=0 status_count=0 commit_count=0 digest_count=0 timestamp_count=0 hostname_count=0 completed=0 pending=0 directory_pending=0 + declare -A seen_file_paths=() seen_directory_paths=() + stage1_validated_file_paths=() + stage1_validated_pending_paths=() + stage1_validated_created_directories=() + stage1_validated_pending_directories=() + stage1_validated_existing_directories=() + while IFS= read -r line || [ -n "$line" ]; do + number=$((number + 1)) + [ -n "$line" ] || return 1 + IFS='|' read -r type f1 f2 f3 f4 f5 f6 extra <<< "$line" + [ -z "$extra" ] || return 1 + delimiter_text=${line//[^|]/} + delimiter_count=${#delimiter_text} + case "$type" in + FORMAT) [ "$delimiter_count" -eq 2 ] || return 1 ;; + STATUS|SOURCE_COMMIT|ARTIFACT_MANIFEST_SHA256|TIMESTAMP|HOSTNAME) [ "$delimiter_count" -eq 1 ] || return 1 ;; + FILE|FILE_PENDING) [ "$delimiter_count" -eq 6 ] || return 1 ;; + DIR_PENDING|DIR_CREATED|DIR_EXISTING) [ "$delimiter_count" -eq 5 ] || return 1 ;; + *) return 1 ;; + esac + if [ "$number" -eq 1 ] && [ "$type" != FORMAT ]; then return 1; fi + if [ "$number" -eq 2 ] && [ "$type" != STATUS ]; then return 1; fi + if [ "$number" -eq 3 ] && [ "$type" != SOURCE_COMMIT ]; then return 1; fi + if [ "$number" -eq 4 ] && [ "$type" != ARTIFACT_MANIFEST_SHA256 ]; then return 1; fi + if [ "$number" -eq 5 ] && [ "$type" != TIMESTAMP ]; then return 1; fi + if [ "$number" -eq 6 ] && [ "$type" != HOSTNAME ]; then return 1; fi + if [ "$number" -gt 6 ] && { [ "$type" = FORMAT ] || [ "$type" = STATUS ] || [ "$type" = SOURCE_COMMIT ] || [ "$type" = ARTIFACT_MANIFEST_SHA256 ] || [ "$type" = TIMESTAMP ] || [ "$type" = HOSTNAME ]; }; then return 1; fi + case "$type" in + FORMAT) [ "$f1" = le-app-codex-phase2b-stage1 ] && [ "$f2" = 3 ] && [ -z "$f3$f4$f5$f6" ] || return 1; format_count=$((format_count + 1)) ;; + STATUS) { [ "$f1" = IN_PROGRESS ] || [ "$f1" = COMPLETE ]; } && [ -z "$f2$f3$f4$f5$f6" ] || return 1; status=$f1; status_count=$((status_count + 1)) ;; + SOURCE_COMMIT) [[ "$f1" =~ ^[0-9a-f]{40}$ ]] && [ -z "$f2$f3$f4$f5$f6" ] || return 1; source_commit=$f1; commit_count=$((commit_count + 1)) ;; + ARTIFACT_MANIFEST_SHA256) [[ "$f1" =~ ^[0-9a-f]{64}$ ]] && [ -z "$f2$f3$f4$f5$f6" ] || return 1; artifact_digest=$f1; digest_count=$((digest_count + 1)) ;; + TIMESTAMP) [ -n "$f1" ] && [ -z "$f2$f3$f4$f5$f6" ] || return 1; timestamp=$f1; timestamp_count=$((timestamp_count + 1)) ;; + HOSTNAME) [ "$f1" = "$expected_hostname" ] && [ -z "$f2$f3$f4$f5$f6" ] || return 1; hostname=$f1; hostname_count=$((hostname_count + 1)) ;; + FILE|FILE_PENDING) + [ -n "${stage1_allowed_file_specs[$f1]:-}" ] || return 1 + [[ "$f1" = /* ]] && [[ "$f1" != *'/../'* ]] && [[ "$f1" != */.. ]] || return 1 + [ -z "${seen_file_paths[$f1]:-}" ] && [ -z "${seen_directory_paths[$f1]:-}" ] || return 1 + [ "${stage1_allowed_file_specs[$f1]}" = "$f2|$f3|$f4|$f5|$f6" ] || return 1 + seen_file_paths[$f1]=1 + if [ "$type" = FILE ]; then stage1_validated_file_paths+=("$f1"); completed=$((completed + 1)); else stage1_validated_pending_paths+=("$f1"); pending=$((pending + 1)); fi + ;; + DIR_PENDING|DIR_CREATED|DIR_EXISTING) + [ -n "${stage1_allowed_directory_specs[$f1]:-}" ] || return 1 + [[ "$f1" = /* ]] && [[ "$f1" != *'/../'* ]] && [[ "$f1" != */.. ]] || return 1 + [ -z "${seen_directory_paths[$f1]:-}" ] && [ -z "${seen_file_paths[$f1]:-}" ] || return 1 + [ "${stage1_allowed_directory_specs[$f1]}" = "$f2|$f3|$f4|$f5" ] || return 1 + if { [ "$type" = DIR_PENDING ] || [ "$type" = DIR_CREATED ]; } && [ "${stage1_never_created_directories[$f1]:-0}" -eq 1 ]; then return 1; fi + seen_directory_paths[$f1]=1 + if [ "$type" = DIR_PENDING ]; then stage1_validated_pending_directories+=("$f1"); directory_pending=$((directory_pending + 1)); elif [ "$type" = DIR_CREATED ]; then stage1_validated_created_directories+=("$f1"); else stage1_validated_existing_directories+=("$f1"); fi + ;; + esac + done < "$manifest" + [ "$format_count" -eq 1 ] && [ "$status_count" -eq 1 ] && [ "$commit_count" -eq 1 ] && [ "$digest_count" -eq 1 ] && [ "$timestamp_count" -eq 1 ] && [ "$hostname_count" -eq 1 ] || return 1 + [ "$artifact_digest" = "$expected_artifact_digest" ] || return 1 + [ "${#stage1_allowed_file_specs[@]}" -eq "$expected_complete_count" ] || return 1 + if [ "$status" = COMPLETE ]; then [ "$completed" -eq "$expected_complete_count" ] && [ "$pending" -eq 0 ] && [ "$directory_pending" -eq 0 ] || return 1; fi + if [ "$status" = IN_PROGRESS ]; then [ $((completed + pending)) -le "$expected_complete_count" ] || return 1; fi + stage1_validated_status=$status + stage1_validated_source_commit=$source_commit + stage1_validated_artifact_digest=$artifact_digest + stage1_validated_timestamp=$timestamp + stage1_validated_hostname=$hostname +} From 909f0a8200c79efc205d92618be47e06ebf764fc Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 12 Jul 2026 23:54:05 -0400 Subject: [PATCH 12/14] add Phase 2B Stage 2 verification --- .../MANIFEST.sha256 | 4 + .../phase2b-stage2-verification/README.md | 37 ++ .../phase2b-stage2-verification/ROLLBACK.md | 9 + .../tests/40-verify-stage2.sh | 379 ++++++++++++++++++ .../tests/41-stage2-static-tests.sh | 92 +++++ 5 files changed, 521 insertions(+) create mode 100644 docs/le-app-database-migration/phase2b-stage2-verification/MANIFEST.sha256 create mode 100644 docs/le-app-database-migration/phase2b-stage2-verification/README.md create mode 100644 docs/le-app-database-migration/phase2b-stage2-verification/ROLLBACK.md create mode 100644 docs/le-app-database-migration/phase2b-stage2-verification/tests/40-verify-stage2.sh create mode 100644 docs/le-app-database-migration/phase2b-stage2-verification/tests/41-stage2-static-tests.sh diff --git a/docs/le-app-database-migration/phase2b-stage2-verification/MANIFEST.sha256 b/docs/le-app-database-migration/phase2b-stage2-verification/MANIFEST.sha256 new file mode 100644 index 0000000..0f261b2 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage2-verification/MANIFEST.sha256 @@ -0,0 +1,4 @@ +0a1cffbe98e0cdd7e70b02c92a6e14ca51b615f3888a405627abb1bd054bafef README.md +e41030a9850097ad91899ec14b2ce1a85f218fddf9b38aa317663450b29b6470 ROLLBACK.md +19b8de84c9e0e6025d66835a2eb834a5ee6c1eb6599e8650ce287f0cf069950e tests/40-verify-stage2.sh +10d4e45f0508e7dd6024b52a58e2848cb792f72a9e3cc070bf4c316df1ee4880 tests/41-stage2-static-tests.sh diff --git a/docs/le-app-database-migration/phase2b-stage2-verification/README.md b/docs/le-app-database-migration/phase2b-stage2-verification/README.md new file mode 100644 index 0000000..cc02260 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage2-verification/README.md @@ -0,0 +1,37 @@ +# Phase 2B Stage 2 — installed-state verification and daemon reload + +Status: prepared for review only, 2026-07-12. Do not run until these uncommitted files have been reviewed and committed. Stage 2 does not authorize runtime activation. + +## Scope + +`tests/40-verify-stage2.sh` is the only Stage 2 operator entry point. It runs as root and: + +1. requires a clean committed worktree and verifies this Stage 2 manifest, the unchanged Stage 1 artifact manifest, and the Stage 1 inventory checksums; +2. proves that the Stage 1 artifact directory is unchanged since the commit recorded in the installed checksummed state; +3. strictly resolves `/var/lib/le-app-codex-phase2b/stage1/current`, requires a `COMPLETE` state with exactly 16 files and no pending records, and binds its manifest digest to the recorded source commit; +4. verifies all 16 destinations against the committed sources and state records by SHA-256, byte size, UID, GID, mode, regular-file type, and single-link count; +5. confirms the runtime is inert, then runs the sole Stage 2 mutation: `systemctl daemon-reload`; +6. validates the installed system units and drop-ins with `systemd-analyze verify`, `systemctl cat`, and exact or membership-checked `systemctl show` properties; parses the installed Docker unit with an explicit user-unit search path without starting a user manager; +7. runs root-capable `nft --check -f /etc/nftables.d/50-le-app-codex.nft` and never loads the policy; +8. explicitly rechecks the namespace helper and rootless launcher hashes; and +9. repeats every inert-state assertion after the reload and static validation. + +The inert-state gates require the approval marker, namespace, namespace path, project veth, rootless Docker socket, lingering, UID-1200 processes, and both Phase 2B nftables tables to be absent. They require `le-app-codex-netns.service` and `user@1200.service` to be inactive and the namespace service to remain static rather than enabled. + +## Explicit exclusions + +Stage 2 does not create the approval marker; start, restart, or enable any unit; use `systemctl --user`; enable lingering; invoke the namespace helper; create a namespace or veth; load nftables; start Docker or Codex; authenticate Codex; copy a repository, migration source, data, or secrets; contact production services; or modify production. + +The script makes no cleanup attempt after a failure. It preserves evidence and exits nonzero. See `ROLLBACK.md` for the Stage 2 no-op rollback and the separately approval-gated Stage 1 removal path. + +## Review and future execution + +Before committing, run only the non-operational `tests/41-stage2-static-tests.sh`. It checks syntax, manifests, immutable Stage 1 bytes, prohibited command literals, exact 16-file specifications, and the existing temporary-filesystem Stage 1 state tests. It never runs the Stage 2 operator script. + +After review, commit the Stage 2 package. From a clean checkout of that commit, the future operator invocation is: + +```console +sudo /bin/bash docs/le-app-database-migration/phase2b-stage2-verification/tests/40-verify-stage2.sh +``` + +Require the final summary to report `daemon_reload=1`, `installed_files=16`, and `failures=0`. Any other result is a hard stop; do not continue to namespace or runtime activation. diff --git a/docs/le-app-database-migration/phase2b-stage2-verification/ROLLBACK.md b/docs/le-app-database-migration/phase2b-stage2-verification/ROLLBACK.md new file mode 100644 index 0000000..5b37205 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage2-verification/ROLLBACK.md @@ -0,0 +1,9 @@ +# Phase 2B Stage 2 rollback and failure handling + +Stage 2 installs and removes no files. Its only mutation is `systemctl daemon-reload`, which refreshes the system manager's view of the already-installed Stage 1 unit files and drop-ins. A successful or failed Stage 2 run does not create a runtime resource that needs teardown. + +If Stage 2 fails before `daemon-reload`, it has changed nothing. Preserve the output, correct only the reviewed repository or installed-state discrepancy, and rerun the entire verifier from a clean committed worktree. + +If Stage 2 fails after `daemon-reload`, do not start, enable, or clean up any service. Preserve the output and require the postconditions to remain inert: no approval marker; inactive and non-enabled `le-app-codex-netns.service`; inactive `user@1200.service`; no linger, UID-1200 processes, Docker socket, namespace, project veth, or Phase 2B nftables tables. Repair only through a separately reviewed change, then rerun the complete Stage 2 verifier. Another `daemon-reload` after repaired unit bytes is sufficient to refresh the manager; no service action is authorized. + +Full Stage 1 removal is outside Stage 2 and requires separate approval. If approved while every inert-state gate still passes, use only the unchanged Stage 1 `tests/31-rollback-stage1.sh` from the clean artifact worktree whose manifest digest matches the installed state. After that rollback succeeds, run `systemctl daemon-reload` to discard the removed unit definitions, then verify the Stage 1 files and state are absent and all runtime-negative assertions still pass. Never use Stage 1 rollback after an approval marker, namespace, firewall table, user manager, Docker daemon, or Codex process has been activated. diff --git a/docs/le-app-database-migration/phase2b-stage2-verification/tests/40-verify-stage2.sh b/docs/le-app-database-migration/phase2b-stage2-verification/tests/40-verify-stage2.sh new file mode 100644 index 0000000..304d42d --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage2-verification/tests/40-verify-stage2.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C +export PATH=/usr/bin:/bin +export SYSTEMD_COLORS=0 +export SYSTEMD_LOG_COLOR=0 +export SYSTEMD_PAGER=cat + +failures=0 +verified_files=0 +daemon_reload=0 +stage2_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +migration_root=$(CDPATH= cd -- "$stage2_root/.." 2>/dev/null && pwd) +stage1_root=$migration_root/phase2b-contained-runtime +repo_root=$(CDPATH= cd -- "$migration_root/../.." 2>/dev/null && pwd) +payload_root=$stage1_root/payload +state_parent=/var/lib/le-app-codex-phase2b +state_transaction=$state_parent/stage1 +installed_policy=/etc/nftables.d/50-le-app-codex.nft +approval_marker=/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED +context_hash=1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535 +context_source=$payload_root/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +context_destination=/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } + +verify_committed_manifest() { + local package_root=$1 label=$2 package_relative manifest line expected path actual committed rc tracked relative + local record_count=0 tree_count=0 + declare -A listed=() + package_relative=${package_root#"$repo_root"/} + manifest=$package_root/MANIFEST.sha256 + if [ -f "$manifest" ] && [ ! -L "$manifest" ]; then pass "$label manifest is a regular file"; else fail "$label manifest missing or unsafe"; return; fi + tracked=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-files --error-unmatch -- "$package_relative/MANIFEST.sha256" 2>/dev/null) + if [ "$tracked" = "$package_relative/MANIFEST.sha256" ]; then pass "$label manifest is tracked"; else fail "$label manifest is not tracked"; fi + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^([0-9a-f]{64})\ \ (.+)$ ]]; then + expected=${BASH_REMATCH[1]} + path=${BASH_REMATCH[2]} + else + fail "$label malformed manifest record" + continue + fi + if [[ "$path" = /* ]] || [[ "$path" = *'|'* ]] || [[ "$path" = *'/../'* ]] || [[ "$path" = ../* ]] || [[ "$path" = */.. ]] || [ "$path" = MANIFEST.sha256 ] || [ -n "${listed[$path]:-}" ]; then + fail "$label unsafe or duplicate manifest path $path" + continue + fi + listed["$path"]=1 + record_count=$((record_count + 1)) + actual=$(/usr/bin/sha256sum "$package_root/$path" 2>/dev/null | /usr/bin/cut -d' ' -f1) + [ "$actual" = "$expected" ] && pass "$label working hash $path" || fail "$label working hash mismatch $path" + relative=$package_relative/$path + tracked=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-files --error-unmatch -- "$relative" 2>/dev/null) + if [ "$tracked" = "$relative" ]; then + committed=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" show "HEAD:$relative" 2>/dev/null | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + rc=$? + if [ "$rc" -eq 0 ] && [ "$committed" = "$expected" ]; then pass "$label committed hash $path"; else fail "$label committed hash mismatch $path"; fi + else + fail "$label untracked manifest path $path" + fi + done < "$manifest" + while IFS= read -r tracked; do + relative=${tracked#"$package_relative"/} + [ "$relative" = MANIFEST.sha256 ] && continue + tree_count=$((tree_count + 1)) + [ -n "${listed[$relative]:-}" ] || fail "$label manifest omits tracked path $relative" + done < <(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-tree -r --name-only HEAD -- "$package_relative" 2>/dev/null) + if [ "$record_count" -gt 0 ] && [ "$record_count" -eq "$tree_count" ]; then pass "$label manifest covers $record_count committed files"; else fail "$label manifest/tree count mismatch records=$record_count tree=$tree_count"; fi +} + +verify_state_tree() { + local entry name generation generation_name child child_name child_count checksum_record expected_checksum checksum_lines + local current_count=0 generations_count=0 generation_count=0 + if [ -d "$state_parent" ] && [ ! -L "$state_parent" ] && [ "$(/usr/bin/stat -c '%u:%g:%a' "$state_parent" 2>/dev/null)" = 0:0:700 ]; then pass 'Stage 1 state parent metadata'; else fail 'Stage 1 state parent metadata'; return; fi + while IFS= read -r -d '' entry; do + name=${entry##*/} + if [ "$name" = stage1 ]; then + [ -d "$entry" ] && [ ! -L "$entry" ] || fail 'Stage 1 state transaction is not a real directory' + else + fail "unexpected state-parent entry $entry" + fi + done < <(/usr/bin/find "$state_parent" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + while IFS= read -r -d '' entry; do + name=${entry##*/} + case "$name" in + current) stage1_state_verify_file "$entry" 0 0 0600 && current_count=$((current_count + 1)) || fail 'unsafe Stage 1 current pointer' ;; + generations) stage1_state_path_is_real_directory "$entry" && [ "$(/usr/bin/stat -c '%u:%g:%a' "$entry" 2>/dev/null)" = 0:0:700 ] && generations_count=$((generations_count + 1)) || fail 'unsafe Stage 1 generations directory' ;; + *) fail "unexpected Stage 1 state entry $entry" ;; + esac + done < <(/usr/bin/find "$state_transaction" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$current_count" -eq 1 ] || fail 'Stage 1 state requires exactly one current pointer' + [ "$generations_count" -eq 1 ] || fail 'Stage 1 state requires exactly one generations directory' + while IFS= read -r -d '' generation; do + generation_name=${generation##*/} + generation_count=$((generation_count + 1)) + [[ "$generation_name" =~ ^gen-[0-9]{8}T[0-9]{6}Z-[0-9]+-[0-9]+$ ]] || fail "invalid Stage 1 generation name $generation_name" + stage1_state_path_is_real_directory "$generation" && [ "$(/usr/bin/stat -c '%u:%g:%a' "$generation" 2>/dev/null)" = 0:0:700 ] || fail "unsafe Stage 1 generation $generation" + child_count=0 + while IFS= read -r -d '' child; do + child_name=${child##*/} + if [ "$child_name" != manifest ] && [ "$child_name" != manifest.sha256 ]; then fail "unexpected generation content $child"; fi + stage1_state_verify_file "$child" 0 0 0600 || fail "unsafe generation file $child" + child_count=$((child_count + 1)) + done < <(/usr/bin/find "$generation" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$child_count" -eq 2 ] || fail "generation must contain exactly two files $generation" + checksum_lines=$(/usr/bin/awk 'END { print NR+0 }' "$generation/manifest.sha256" 2>/dev/null) + checksum_record=$(<"$generation/manifest.sha256") + expected_checksum="$(/usr/bin/sha256sum "$generation/manifest" 2>/dev/null | /usr/bin/cut -d' ' -f1) manifest" + if [ "$checksum_lines" -eq 1 ] && [ "$checksum_record" = "$expected_checksum" ] && (cd "$generation" && /usr/bin/sha256sum -c manifest.sha256 >/dev/null 2>&1); then pass "checksummed state generation $generation_name"; else fail "generation checksum mismatch $generation"; fi + done < <(/usr/bin/find "$state_transaction/generations" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$generation_count" -gt 0 ] || fail 'Stage 1 state has no generations' +} + +verify_directory_record() { + local path=$1 spec expected_uid expected_gid expected_mode expected_kind actual + spec=${stage1_allowed_directory_specs[$path]} + IFS='|' read -r expected_uid expected_gid expected_mode expected_kind <<< "$spec" + actual=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + if [ ! -L "$path" ] && [ "$actual" = "$expected_uid:$expected_gid:${expected_mode#0}:directory" ]; then pass "recorded directory $path"; else fail "recorded directory mismatch $path"; fi +} + +verify_installed_file() { + local path=$1 label=${2:-$1} spec expected_hash expected_size expected_uid expected_gid expected_mode actual_hash actual_size actual_stat links + spec=${stage1_allowed_file_specs[$path]:-} + if [ -z "$spec" ]; then fail "$label has no committed Stage 1 specification"; return; fi + IFS='|' read -r expected_hash expected_size expected_uid expected_gid expected_mode <<< "$spec" + actual_hash=$(/usr/bin/sha256sum "$path" 2>/dev/null | /usr/bin/cut -d' ' -f1) + actual_size=$(/usr/bin/stat -c '%s' "$path" 2>/dev/null) + actual_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + links=$(/usr/bin/stat -c '%h' "$path" 2>/dev/null) + if [ ! -L "$path" ] && [ "$actual_hash" = "$expected_hash" ] && [ "$actual_size" = "$expected_size" ] && [ "$actual_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:regular file" ] && [ "$links" = 1 ]; then pass "$label hash/size/metadata"; else fail "$label hash, size, metadata, type, or link-count mismatch"; fi +} + +verify_inert_state() { + local phase=$1 active sub enabled enabled_rc processes process_rc namespaces namespace_rc links link_rc tables table_rc + if [ ! -e "$approval_marker" ] && [ ! -L "$approval_marker" ]; then pass "$phase approval marker absent"; else fail "$phase approval marker present"; fi + active=$(/usr/bin/systemctl show le-app-codex-netns.service --property=ActiveState --value 2>/dev/null) + sub=$(/usr/bin/systemctl show le-app-codex-netns.service --property=SubState --value 2>/dev/null) + if [ "$active" = inactive ] && [ "$sub" = dead ]; then pass "$phase namespace service inactive"; else fail "$phase namespace service state ${active:-unknown}/${sub:-unknown}"; fi + enabled=$(/usr/bin/systemctl is-enabled le-app-codex-netns.service 2>/dev/null) + enabled_rc=$? + if { [ "$enabled_rc" -eq 0 ] || [ "$enabled_rc" -eq 1 ]; } && [ "$enabled" = static ]; then pass "$phase namespace service static, not enabled"; else fail "$phase namespace service enablement ${enabled:-unknown} rc=$enabled_rc"; fi + active=$(/usr/bin/systemctl show user@1200.service --property=ActiveState --value 2>/dev/null) + sub=$(/usr/bin/systemctl show user@1200.service --property=SubState --value 2>/dev/null) + if [ "$active" = inactive ] && [ "$sub" = dead ]; then pass "$phase user@1200.service inactive"; else fail "$phase user@1200.service state ${active:-unknown}/${sub:-unknown}"; fi + if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -L /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ] && [ ! -L /var/lib/systemd/linger/1200 ]; then pass "$phase lingering absent"; else fail "$phase lingering present"; fi + processes=$(/usr/bin/pgrep -u 1200 2>/dev/null) + process_rc=$? + if [ "$process_rc" -eq 1 ] && [ -z "$processes" ]; then pass "$phase UID 1200 has no processes"; elif [ "$process_rc" -eq 0 ]; then fail "$phase UID 1200 processes found: $processes"; else fail "$phase UID 1200 process query failed rc=$process_rc"; fi + if [ ! -e /run/user/1200/docker.sock ] && [ ! -L /run/user/1200/docker.sock ]; then pass "$phase rootless Docker socket absent"; else fail "$phase rootless Docker socket present"; fi + namespaces=$(/usr/bin/ip netns list 2>/dev/null) + namespace_rc=$? + if [ "$namespace_rc" -ne 0 ]; then fail "$phase namespace query failed rc=$namespace_rc"; elif printf '%s\n' "$namespaces" | /usr/bin/awk '$1 == "le-app-codex" { found=1 } END { exit !found }'; then fail "$phase le-app-codex namespace present"; elif [ -e /run/netns/le-app-codex ] || [ -L /run/netns/le-app-codex ]; then fail "$phase namespace path present"; else pass "$phase namespace absent"; fi + links=$(/usr/bin/ip -o link show 2>/dev/null) + link_rc=$? + if [ "$link_rc" -ne 0 ]; then fail "$phase link query failed rc=$link_rc"; elif printf '%s\n' "$links" | /usr/bin/awk -F': ' '$2 ~ /^lecodex-host(@|:|$)/ { found=1 } END { exit !found }'; then fail "$phase project veth present"; else pass "$phase project veth absent"; fi + tables=$(/usr/bin/nft list tables 2>/dev/null) + table_rc=$? + if [ "$table_rc" -ne 0 ]; then + fail "$phase nftables query failed rc=$table_rc" + else + if printf '%s\n' "$tables" | /usr/bin/awk '$1 == "table" && $2 == "inet" && $3 == "le_app_codex" { found=1 } END { exit !found }'; then fail "$phase inet Phase 2B table present"; else pass "$phase inet Phase 2B table absent"; fi + if printf '%s\n' "$tables" | /usr/bin/awk '$1 == "table" && $2 == "ip" && $3 == "le_app_codex_nat" { found=1 } END { exit !found }'; then fail "$phase NAT Phase 2B table present"; else pass "$phase NAT Phase 2B table absent"; fi + fi +} + +verify_analyze() { + local label=$1 output rc + shift + output=$("$@" 2>&1) + rc=$? + [ -z "$output" ] || printf '%s\n' "$output" + if [ "$rc" -eq 0 ]; then pass "$label"; else fail "$label rc=$rc"; fi +} + +verify_systemctl_cat() { + local unit=$1 output rc expected + shift + output=$(/usr/bin/systemctl cat --no-pager "$unit" 2>&1) + rc=$? + printf '%s\n' "$output" + if [ "$rc" -eq 0 ]; then pass "systemctl cat $unit"; else fail "systemctl cat $unit rc=$rc"; fi + for expected in "$@"; do + if printf '%s\n' "$output" | /usr/bin/grep -Fqx "# $expected"; then pass "$unit includes $expected"; else fail "$unit omits $expected"; fi + done +} + +capture_systemctl_show() { + local unit=$1 + shift + systemctl_show_output=$(/usr/bin/systemctl show --no-pager "$unit" "$@" 2>&1) + systemctl_show_rc=$? + printf '%s\n' "$systemctl_show_output" + if [ "$systemctl_show_rc" -eq 0 ]; then pass "systemctl show $unit"; else fail "systemctl show $unit rc=$systemctl_show_rc"; fi +} + +expect_show_exact() { + local label=$1 output=$2 property=$3 expected=$4 line count=0 actual= + while IFS= read -r line; do + if [[ "$line" = "$property="* ]]; then actual=${line#*=}; count=$((count + 1)); fi + done <<< "$output" + if [ "$count" -eq 1 ] && [ "$actual" = "$expected" ]; then pass "$label $property=$expected"; else fail "$label $property expected $expected, got ${actual:-missing}"; fi +} + +expect_show_word() { + local label=$1 output=$2 property=$3 expected=$4 line count=0 actual= word found=0 + while IFS= read -r line; do + if [[ "$line" = "$property="* ]]; then actual=${line#*=}; count=$((count + 1)); fi + done <<< "$output" + for word in $actual; do [ "$word" = "$expected" ] && found=1; done + if [ "$count" -eq 1 ] && [ "$found" -eq 1 ]; then pass "$label $property contains $expected"; else fail "$label $property omits $expected"; fi +} + +if [ "$(/usr/bin/id -u)" = 0 ]; then pass 'running as root'; else fail 'must run as root'; fi +if [ -n "$repo_root" ] && [ -e "$repo_root/.git" ] && [ ! -L "$repo_root/.git" ] && [ -d "$stage1_root" ] && [ -d "$stage2_root" ]; then pass 'repository and artifact roots resolved'; else fail 'repository or artifact root resolution'; fi +repo_head=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" rev-parse HEAD 2>/dev/null) +repo_status=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" status --porcelain --untracked-files=all 2>/dev/null) +repo_status_rc=$? +if [[ "$repo_head" =~ ^[0-9a-f]{40}$ ]]; then pass "committed repository HEAD $repo_head"; else fail 'repository HEAD unavailable'; fi +if [ "$repo_status_rc" -eq 0 ] && [ -z "$repo_status" ]; then pass 'repository worktree clean'; else fail 'repository worktree dirty or unreadable'; fi + +verify_committed_manifest "$stage2_root" 'Stage 2' +verify_committed_manifest "$stage1_root" 'Stage 1' +if (cd "$stage1_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS); then pass 'Stage 1 inventory checksums'; else fail 'Stage 1 inventory checksums'; fi + +source "$stage1_root/tests/stage1-state-lib.sh" + +sources=( + "$payload_root/usr/local/libexec/dockerd-rootless-29.6.1.sh" + "$payload_root/usr/local/libexec/le-app-codex-netns" + "$payload_root/etc/le-app-codex-runtime/resolv.conf" + "$payload_root/etc/nftables.d/50-le-app-codex.nft" + "$payload_root/etc/systemd/system/le-app-codex-netns.service" + "$payload_root/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf" + "$payload_root/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf" + "$payload_root/srv/le-app-codex/home/.config/systemd/user/docker.service" + "$payload_root/srv/le-app-codex/home/.config/docker/daemon.json" + "$payload_root/srv/le-app-codex/home/.docker/config.json" + "$context_source" + "$stage1_root/tests/00-read-only-preflight.sh" + "$stage1_root/tests/00-root-preinstall-validate.sh" + "$stage1_root/tests/10-inert-containment.sh" + "$stage1_root/tests/20-rootless-docker.sh" + "$stage1_root/tests/run-inert-without-user-manager.sh" +) +destinations=( + /usr/local/libexec/dockerd-rootless-29.6.1.sh + /usr/local/libexec/le-app-codex-netns + /etc/le-app-codex-runtime/resolv.conf + /etc/nftables.d/50-le-app-codex.nft + /etc/systemd/system/le-app-codex-netns.service + /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + /srv/le-app-codex/home/.config/systemd/user/docker.service + /srv/le-app-codex/home/.config/docker/daemon.json + /srv/le-app-codex/home/.docker/config.json + "$context_destination" + /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh + /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh + /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh + /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh +) +uids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +gids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) +modes=(0755 0755 0644 0644 0644 0644 0644 0640 0640 0640 0640 0750 0750 0750 0750 0750) +allowed_directories=( + /srv/le-app-codex/home/.docker/contexts/meta/$context_hash /srv/le-app-codex/home/.docker/contexts/meta /srv/le-app-codex/home/.docker/contexts /srv/le-app-codex/home/.docker + /srv/le-app-codex/home/.config/systemd/user /srv/le-app-codex/home/.config/systemd /srv/le-app-codex/home/.config/docker /srv/le-app-codex/home/.config /srv/le-app-codex/phase2b-tests + /etc/systemd/system/user@1200.service.d /etc/systemd/system/user-1200.slice.d /etc/le-app-codex-runtime /etc/nftables.d /usr/local/libexec "$state_parent" + /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc +) +directory_uids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 0 0 0 0) +directory_gids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 1200 0 0 0) +directory_modes=(0750 0750 0750 0750 0750 0750 0750 0750 0750 0755 0755 0755 0755 0755 0700 0700 0750 0755 0755 0755) +directory_kinds=(project project project project project project project project project project project project shared shared state project-boundary project-boundary shared-base shared-base shared-base) +declare -A stage1_allowed_file_specs=() stage1_allowed_directory_specs=() stage1_never_created_directories=() +for index in "${!destinations[@]}"; do + source_hash=$(/usr/bin/sha256sum "${sources[$index]}" 2>/dev/null | /usr/bin/cut -d' ' -f1) + source_size=$(/usr/bin/stat -c '%s' "${sources[$index]}" 2>/dev/null) + if [[ "$source_hash" =~ ^[0-9a-f]{64}$ ]] && [[ "$source_size" =~ ^[0-9]+$ ]]; then + stage1_allowed_file_specs["${destinations[$index]}"]="$source_hash|$source_size|${uids[$index]}|${gids[$index]}|${modes[$index]}" + else + fail "committed Stage 1 source unreadable ${sources[$index]}" + fi +done +for index in "${!allowed_directories[@]}"; do stage1_allowed_directory_specs["${allowed_directories[$index]}"]="${directory_uids[$index]}|${directory_gids[$index]}|${directory_modes[$index]}|${directory_kinds[$index]}"; done +for directory in /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc; do stage1_never_created_directories["$directory"]=1; done +if [ "${#sources[@]}" -eq 16 ] && [ "${#destinations[@]}" -eq 16 ] && [ "${#stage1_allowed_file_specs[@]}" -eq 16 ]; then pass 'exact 16-file Stage 1 specification'; else fail 'Stage 1 specification cardinality'; fi + +current_stage1_digest=$(/usr/bin/sha256sum "$stage1_root/MANIFEST.sha256" 2>/dev/null | /usr/bin/cut -d' ' -f1) +stage1_state_resolve_current "$state_transaction" 0 0 0600 || fail 'authoritative Stage 1 state generation' +if [ "$failures" -eq 0 ]; then verify_state_tree; fi +if [ "$failures" -eq 0 ]; then stage1_state_validate_records "$stage1_current_manifest" "$current_stage1_digest" "$(/usr/bin/hostname)" 16 || fail 'authoritative Stage 1 state records'; fi +completed_directory_records=$((${#stage1_validated_created_directories[@]} + ${#stage1_validated_existing_directories[@]})) +if [ "$failures" -eq 0 ] && [ "$stage1_validated_status" = COMPLETE ] && [ "${#stage1_validated_file_paths[@]}" -eq 16 ] && [ "${#stage1_validated_pending_paths[@]}" -eq 0 ] && [ "${#stage1_validated_pending_directories[@]}" -eq 0 ] && [ "$completed_directory_records" -eq "${#stage1_allowed_directory_specs[@]}" ] && [ "$completed_directory_records" -eq 20 ]; then pass 'Stage 1 state COMPLETE with exact file/directory records and nothing pending'; else fail 'Stage 1 state is not exact COMPLETE'; fi + +if [ "$failures" -eq 0 ]; then + if /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" cat-file -e "$stage1_validated_source_commit^{commit}" 2>/dev/null; then pass "recorded source commit exists $stage1_validated_source_commit"; else fail 'recorded source commit missing'; fi + if /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" merge-base --is-ancestor "$stage1_validated_source_commit" HEAD 2>/dev/null; then pass 'recorded source commit is an ancestor of HEAD'; else fail 'recorded source commit is not an ancestor of HEAD'; fi + stage1_relative=${stage1_root#"$repo_root"/} + historical_digest=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" show "$stage1_validated_source_commit:$stage1_relative/MANIFEST.sha256" 2>/dev/null | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + historical_rc=$? + if [ "$historical_rc" -eq 0 ] && [ "$historical_digest" = "$stage1_validated_artifact_digest" ]; then pass 'recorded artifact digest matches recorded commit'; else fail 'recorded artifact digest/source commit mismatch'; fi + if /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" diff --quiet "$stage1_validated_source_commit" HEAD -- "$stage1_relative"; then pass 'Stage 1 artifact tree unchanged since installation commit'; else fail 'Stage 1 artifact tree changed since installation commit'; fi +fi + +if [ "$failures" -eq 0 ]; then + for destination in "${destinations[@]}"; do verify_installed_file "$destination"; done + for directory in "${stage1_validated_created_directories[@]}" "${stage1_validated_existing_directories[@]}"; do [ -z "$directory" ] || verify_directory_record "$directory"; done + [ "$failures" -ne 0 ] || verified_files=16 +fi + +if [ "$failures" -eq 0 ]; then verify_inert_state 'pre-reload'; fi +if [ "$failures" -ne 0 ]; then + printf 'Phase 2B Stage 2 daemon_reload=0 installed_files=%s failures=%s\n' "$verified_files" "$failures" + exit 1 +fi + +if /usr/bin/systemctl daemon-reload; then daemon_reload=1; pass 'systemctl daemon-reload'; else fail 'systemctl daemon-reload'; fi + +if [ "$failures" -eq 0 ]; then + verify_analyze 'installed system units and drop-ins' /usr/bin/systemd-analyze verify --recursive-errors=no le-app-codex-netns.service user@1200.service user-1200.slice + user_unit_path=/srv/le-app-codex/home/.config/systemd/user:/etc/systemd/user:/run/systemd/user:/usr/local/lib/systemd/user:/usr/lib/systemd/user + verify_analyze 'installed Docker unit with explicit user-unit path' /usr/bin/env SYSTEMD_UNIT_PATH="$user_unit_path" /usr/bin/systemd-analyze verify --recursive-errors=no docker.service + + verify_systemctl_cat le-app-codex-netns.service /etc/systemd/system/le-app-codex-netns.service + verify_systemctl_cat user@1200.service /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + verify_systemctl_cat user-1200.slice /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + + capture_systemctl_show le-app-codex-netns.service --property=LoadState --property=ActiveState --property=SubState --property=UnitFileState --property=FragmentPath --property=Type --property=RemainAfterExit + netns_show=$systemctl_show_output + expect_show_exact le-app-codex-netns.service "$netns_show" LoadState loaded + expect_show_exact le-app-codex-netns.service "$netns_show" ActiveState inactive + expect_show_exact le-app-codex-netns.service "$netns_show" SubState dead + expect_show_exact le-app-codex-netns.service "$netns_show" UnitFileState static + expect_show_exact le-app-codex-netns.service "$netns_show" FragmentPath /etc/systemd/system/le-app-codex-netns.service + expect_show_exact le-app-codex-netns.service "$netns_show" Type oneshot + expect_show_exact le-app-codex-netns.service "$netns_show" RemainAfterExit yes + + capture_systemctl_show user@1200.service --property=LoadState --property=ActiveState --property=SubState --property=DropInPaths --property=Requires --property=After --property=NetworkNamespacePath --property=ProtectSystem --property=PrivateTmp --property=ProtectHome --property=ProtectProc --property=ProcSubset --property=DevicePolicy + user_show=$systemctl_show_output + expect_show_exact user@1200.service "$user_show" LoadState loaded + expect_show_exact user@1200.service "$user_show" ActiveState inactive + expect_show_exact user@1200.service "$user_show" SubState dead + expect_show_word user@1200.service "$user_show" DropInPaths /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + expect_show_word user@1200.service "$user_show" Requires le-app-codex-netns.service + expect_show_word user@1200.service "$user_show" After le-app-codex-netns.service + expect_show_exact user@1200.service "$user_show" NetworkNamespacePath /run/netns/le-app-codex + expect_show_exact user@1200.service "$user_show" ProtectSystem strict + expect_show_exact user@1200.service "$user_show" PrivateTmp yes + expect_show_exact user@1200.service "$user_show" ProtectHome yes + expect_show_exact user@1200.service "$user_show" ProtectProc invisible + expect_show_exact user@1200.service "$user_show" ProcSubset all + expect_show_exact user@1200.service "$user_show" DevicePolicy closed + + capture_systemctl_show user-1200.slice --property=LoadState --property=ActiveState --property=DropInPaths --property=CPUQuotaPerSecUSec --property=MemoryHigh --property=MemoryMax --property=MemorySwapMax --property=TasksMax + slice_show=$systemctl_show_output + expect_show_exact user-1200.slice "$slice_show" LoadState loaded + expect_show_exact user-1200.slice "$slice_show" ActiveState inactive + expect_show_word user-1200.slice "$slice_show" DropInPaths /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + expect_show_exact user-1200.slice "$slice_show" CPUQuotaPerSecUSec 6s + expect_show_exact user-1200.slice "$slice_show" MemoryHigh 51539607552 + expect_show_exact user-1200.slice "$slice_show" MemoryMax 68719476736 + expect_show_exact user-1200.slice "$slice_show" MemorySwapMax 17179869184 + expect_show_exact user-1200.slice "$slice_show" TasksMax 4096 +fi + +if [ "$failures" -eq 0 ]; then + if /usr/bin/nft --check -f "$installed_policy"; then pass 'installed nftables policy syntax checked without load'; else fail 'installed nftables policy syntax check'; fi + verify_installed_file /usr/local/libexec/le-app-codex-netns 'installed namespace helper' + verify_installed_file /usr/local/libexec/dockerd-rootless-29.6.1.sh 'installed rootless launcher' +fi + +verify_inert_state 'post-reload' +printf 'Phase 2B Stage 2 daemon_reload=%s installed_files=%s failures=%s\n' "$daemon_reload" "$verified_files" "$failures" +[ "$daemon_reload" -eq 1 ] && [ "$verified_files" -eq 16 ] && [ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-stage2-verification/tests/41-stage2-static-tests.sh b/docs/le-app-database-migration/phase2b-stage2-verification/tests/41-stage2-static-tests.sh new file mode 100644 index 0000000..8ea429e --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage2-verification/tests/41-stage2-static-tests.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C + +failures=0 +stage2_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +migration_root=$(CDPATH= cd -- "$stage2_root/.." 2>/dev/null && pwd) +stage1_root=$migration_root/phase2b-contained-runtime +operator=$stage2_root/tests/40-verify-stage2.sh + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } + +array_line_count() { + local name=$1 + /usr/bin/awk -v opening="$name=(" ' + $0 == opening { inside=1; next } + inside && $0 == ")" { exit } + inside { count++ } + END { print count+0 } + ' "$operator" +} + +emit_array_lines() { + local file=$1 name=$2 + /usr/bin/awk -v opening="$name=(" ' + $0 == opening { inside=1; next } + inside && $0 == ")" { exit } + inside { sub(/^[[:space:]]+/, ""); print } + ' "$file" +} + +verify_working_manifest_coverage() { + local line path file relative records=0 files=0 + declare -A listed=() + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^[0-9a-f]{64}\ \ (.+)$ ]]; then path=${BASH_REMATCH[1]}; else fail 'Stage 2 malformed manifest record'; continue; fi + if [ "$path" = MANIFEST.sha256 ] || [ -n "${listed[$path]:-}" ]; then fail "Stage 2 unsafe or duplicate manifest record $path"; else listed["$path"]=1; records=$((records + 1)); fi + done < "$stage2_root/MANIFEST.sha256" + while IFS= read -r -d '' file; do + relative=${file#"$stage2_root"/} + [ "$relative" = MANIFEST.sha256 ] && continue + files=$((files + 1)) + [ -n "${listed[$relative]:-}" ] || fail "Stage 2 manifest omits $relative" + done < <(/usr/bin/find "$stage2_root" -type f -print0) + if [ "$records" -eq "$files" ] && [ "$records" -gt 0 ]; then pass "Stage 2 manifest covers $records working files"; else fail "Stage 2 manifest coverage records=$records files=$files"; fi +} + +if /bin/bash -n "$operator" && /bin/bash -n "$stage2_root/tests/41-stage2-static-tests.sh"; then pass 'Stage 2 Bash syntax'; else fail 'Stage 2 Bash syntax'; fi +if (cd "$stage1_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'unchanged Stage 1 artifact manifest'; else fail 'Stage 1 artifact manifest'; fi +if (cd "$stage1_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS); then pass 'unchanged Stage 1 inventory'; else fail 'Stage 1 inventory'; fi +if (cd "$stage2_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'Stage 2 artifact manifest'; else fail 'Stage 2 artifact manifest'; fi +verify_working_manifest_coverage + +repo_root=$(/usr/bin/git -C "$stage2_root" rev-parse --show-toplevel 2>/dev/null) +stage1_relative=${stage1_root#"$repo_root"/} +stage1_status=$(/usr/bin/git -C "$repo_root" status --porcelain --untracked-files=all -- "$stage1_relative" 2>/dev/null) +if [ -z "$stage1_status" ] && /usr/bin/git -C "$repo_root" diff --quiet HEAD -- "$stage1_relative"; then pass 'Stage 1 artifact directory byte-for-byte unchanged'; else fail 'Stage 1 artifact directory changed'; fi + +source_count=$(array_line_count sources) +destination_count=$(array_line_count destinations) +if [ "$source_count" -eq 16 ] && [ "$destination_count" -eq 16 ]; then pass 'operator has exact 16 source and destination records'; else fail "operator source/destination counts $source_count/$destination_count"; fi +stage1_rollback=$stage1_root/tests/31-rollback-stage1.sh +if /usr/bin/diff -u <(emit_array_lines "$stage1_rollback" sources) <(emit_array_lines "$operator" sources | /usr/bin/sed 's/\$stage1_root/\$artifact_root/g') >/dev/null && \ + /usr/bin/diff -u <(emit_array_lines "$stage1_rollback" destinations) <(emit_array_lines "$operator" destinations) >/dev/null; then pass 'operator 16-file map exactly matches Stage 1'; else fail 'operator 16-file map differs from Stage 1'; fi +if /usr/bin/grep -Fqx 'uids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200)' "$operator" && \ + /usr/bin/grep -Fqx 'gids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200)' "$operator" && \ + /usr/bin/grep -Fqx 'modes=(0755 0755 0644 0644 0644 0644 0644 0640 0640 0640 0640 0750 0750 0750 0750 0750)' "$operator"; then pass 'operator ownership and mode vectors'; else fail 'operator ownership or mode vectors'; fi + +reload_count=$(/usr/bin/grep -Fc '/usr/bin/systemctl daemon-reload' "$operator") +nft_check_count=$(/usr/bin/grep -Fc '/usr/bin/nft --check -f "$installed_policy"' "$operator") +dockerd_path_count=$(/usr/bin/grep -Fc '/usr/local/libexec/dockerd-rootless-29.6.1.sh' "$operator") +if [ "$reload_count" -eq 1 ]; then pass 'exactly one daemon-reload command'; else fail "daemon-reload command count $reload_count"; fi +if [ "$nft_check_count" -eq 1 ]; then pass 'exactly one nft --check command against installed policy'; else fail "nft --check command count $nft_check_count"; fi +if [ "$dockerd_path_count" -eq 3 ]; then pass 'rootless launcher appears only in source, destination, and hash recheck'; else fail "unexpected rootless launcher literal count $dockerd_path_count"; fi + +if /usr/bin/grep -Eq '/usr/bin/systemctl[[:space:]]+(start|stop|restart|try-restart|reload-or-restart|enable|disable|mask|unmask|preset|reenable)|/usr/bin/systemctl[[:space:]]+--user|/usr/bin/loginctl|/usr/bin/ip[[:space:]]+netns[[:space:]]+(add|delete|exec)|/usr/bin/ip[[:space:]]+link[[:space:]]+(add|delete|set)|/usr/bin/nft[[:space:]]+-f|/usr/bin/(cp|install|ln|mkdir|mv|rm)[[:space:]]' "$operator"; then + fail 'operator contains a prohibited mutation command literal' +else + pass 'operator contains no prohibited mutation command literal' +fi +if /usr/bin/grep -Eq 'OPERATOR-INPUTS-APPROVED.*(touch|install|printf)|le-app-codex-netns[[:space:]]+(up|down)|/usr/bin/docker[[:space:]]+(run|start)|/usr/bin/codex[[:space:]]+(login|auth)' "$operator"; then + fail 'operator contains a prohibited activation literal' +else + pass 'operator contains no prohibited activation literal' +fi + +if /bin/bash "$stage1_root/tests/32-stage1-state-tests.sh"; then pass 'existing non-mutating Stage 1 state tests'; else fail 'Stage 1 state tests'; fi + +printf 'Phase 2B Stage 2 static tests failures=%s\n' "$failures" +[ "$failures" -eq 0 ] From 885dff127e327b6a3b8adbda9df7ce876cbb18e7 Mon Sep 17 00:00:00 2001 From: makearmy Date: Mon, 13 Jul 2026 23:34:36 -0400 Subject: [PATCH 13/14] add Phase 2B Stage 3 namespace activation --- .../MANIFEST.sha256 | 11 + .../README.md | 56 + .../ROLLBACK.md | 17 + .../tests/50-activate-stage3.sh | 197 +++ .../tests/51-rollback-stage3.sh | 431 +++++++ .../tests/52-run-stage1-inert.sh | 219 ++++ .../tests/53-stage3-static-tests.sh | 123 ++ .../tests/54-stage3-failure-path-tests.sh | 632 ++++++++++ .../tests/docker-config/config.json | 1 + .../tests/inert-bin/rm | 6 + .../tests/inert-bin/touch | 9 + .../tests/stage3-common.sh | 1090 +++++++++++++++++ 12 files changed, 2792 insertions(+) create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/ROLLBACK.md create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/52-run-stage1-inert.sh create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/docker-config/config.json create mode 100755 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/rm create mode 100755 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/touch create mode 100644 docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 new file mode 100644 index 0000000..26b8d77 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 @@ -0,0 +1,11 @@ +1e281c255a9a7ba72734c40a947cab4168f70b7e6b733eba7415109a40776a9d README.md +908ab54049c1e810ba5c47812fc51848e9eef8818a9ddc373f841672ab509a57 ROLLBACK.md +136707ac1391bd241efc834c549c0f54d4a849ce832c55457bdeb243cc03800a tests/50-activate-stage3.sh +5c8eceb9b80865ae09bd4ece2a248da4677237abcc7c55d26334020543e8edf8 tests/51-rollback-stage3.sh +9aa43f4b72057ea28ec00309889f75e756a9b36db77e5db758b752990768c0a0 tests/52-run-stage1-inert.sh +de70e85a4e4e6015726dfe3cbad2bdccb2990daae25eeeab272e107d2d68a115 tests/53-stage3-static-tests.sh +ec595556f2842329a8b1b6e88bc291ef8f4d0d98e675baeb4b317da6ae1c8e96 tests/54-stage3-failure-path-tests.sh +ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 tests/docker-config/config.json +1c64518e42afdf490047358c523a8213e989a84a61f51ff0126439db2b485fe3 tests/inert-bin/rm +034b23c09a023ce94d8b7888f0d6f45aed31ba14dbed2efd30c4b0a6322f657b tests/inert-bin/touch +3a3a84df1021e02cc258edbee621ab8abfb9f83bdc17dfa689fb9823b17c8cfd tests/stage3-common.sh diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md new file mode 100644 index 0000000..5c2dc9e --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md @@ -0,0 +1,56 @@ +# Phase 2B Stage 3 — namespace activation and inert acceptance + +Status: prepared for review only, 2026-07-13. Do not execute the activation or rollback scripts from this uncommitted tree. Stage 3 does not authorize the systemd user manager, lingering, rootless Docker, Codex, source/data/secret copying, application startup, service enablement, or production changes. + +## Entry points + +`tests/50-activate-stage3.sh` is the sole activation entry point. `tests/51-rollback-stage3.sh` is the separate Stage 3-only rollback/recovery entry point. Both require root, a clean committed worktree, a validated inherited or newly acquired exclusive lock on the existing root-owned runtime directory, exact Stage 1/2/3 manifests, the pinned Stage 1 state and all 16 installed files plus all 20 directory records, and unchanged Stage 1/2 trees since Stage 2 commit `909f0a8200c79efc205d92618be47e06ebf764fc`. + +Successful Stage 2 execution is an external operator prerequisite confirmed for TITANSERVER. Git provenance can prove which Stage 2 code is trusted; it cannot by itself prove that the prior operator run occurred. + +## Activation behavior + +Before any mutation, activation requires: + +- Stage 1 source `df6b53e63916880bec2c77219b04b010b8b453f1`, artifact-manifest digest `5aaa91b1e6846eda033961dcee392b9657476ad6fd149420979e9309b484445f`, exact `COMPLETE` state, and 16 matching installed files; +- Stage 2 commit and package-manifest provenance, plus a clean committed Stage 3 package; +- installed policy digest `4289b705dbd786281daccb6d5aa660c27b83441f1a34d0cd18590666124be386` and a root `nft --check` that does not load it; +- current public/NAT IPv4 `74.67.173.56`, the committed 41-network rootful Docker configuration, the expected LAN/WireGuard identities, and an already-enabled host IPv4 forward setting that Stage 3 never changes; every Docker read scrubs context/TLS selector variables and uses an explicit `/run/docker.sock` host plus the committed empty client configuration, so it cannot inherit another context or read operator credentials; +- fresh immediate fingerprints of nonproject nftables, stable nonproject host network state, rootful Docker networks/runtime/ports, host listeners, resolver metadata, and public IPv4; and +- no marker, namespace, project veth/table, user manager, linger, UID-1200 process, or rootless socket. + +Immediately before publication and start, the script revalidates the loaded manager state: `systemd-analyze verify`; exact `systemctl cat` fragment/drop-in surfaces for both units; exact namespace fragment path with no drop-ins; current manager cache; no pending job; exact single `ExecStart`/`ExecStop`; oneshot/`RemainAfterExit`; static inactive state; the approval-marker condition; `Before=user@1200.service`; and the reciprocal user-manager `Requires`/`After`, namespace path, fragment, and exact two-drop-in set. + +The script renders and fsyncs a root-owned mode `0600`, single-link version-2 record before atomically renaming it into the approval-marker path and syncing the parent directory. No hard-link publication window exists, and an interrupted write cannot replace the prior authoritative record. Parsing requires the exact canonical one-delimiter, final-newline-complete byte representation, so trailing fields, truncated final writes, and embedded NUL bytes are rejected. The immutable fields include an activation UUID, machine-ID digest, approved Quad9 pair, Stage 1 source and artifact digest, Stage 2 verification commit, Stage 3 activation commit, UTC timestamp, hostname, boot ID, public IPv4, installed-policy digest, and baseline fingerprints. It starts `le-app-codex-netns.service`, then atomically replaces `PREPARED` with an `ACTIVE` record bound to the service invocation, namespace inode, host-veth ifindex/iflink/MAC, both nft table handles, independent counter-normalized table digests, and the combined project-policy digest. + +Post-start validation requires a non-symlink `nsfs` namespace containing exactly loopback and the namespace veth, a typed host inventory with no host-resident peer, reciprocal host/peer ifindex/iflink identity, exact `/30` addresses/routes, no namespace IPv6 address/route, IPv6-disable sysctls, an empty namespace, no listener/published port, exactly the intended two nftables tables, three sets, two filter chains with 4 and 10 rules, and one postrouting masquerade rule. Rule expressions and order are checked against strict nftables JSON envelopes rather than presentation-dependent text; IPv4 set elements receive an additional text cross-check. Each provenance capture derives both independent table hashes and the combined policy hash from the same two raw canonical tables, brackets them with typed handle inventories, then requires a second complete capture to match. This stable exact snapshot is repeated immediately before publishing `ACTIVE`, after network probes, and as the final activation policy gate. It rejects extra nftables object types, DNAT, redirect, and prerouting publication. Counter-delta tests—not connection failure alone—prove the private/LAN/WireGuard/all-39-Docker-gateway/metadata/reserved/public-IP/SMTP/nonapproved-DNS/other-port denials. Both Quad9 servers must answer classic DNS over UDP and TCP. A Quad9-resolved public address must work over TCP 80 and 443 using `curl --resolve` with every uppercase and lowercase proxy variable removed. + +The installed Stage 1 `10-inert-containment.sh` is executed unchanged through `tests/52-run-stage1-inert.sh`. That reviewed wrapper reproduces the installed drop-in's service containment settings, including the resolver bind and inaccessible paths, and points the transient directly at the already-active namespace instead of duplicating the user-manager unit's `[Unit]` dependency declarations. It removes every uppercase/lowercase proxy variable from the transient process environment. The old test's missing `/srv/le-app-codex/tmp` write probe is narrowly redirected by two read-only bound wrappers to the existing approved `build-artifacts` directory and is identity-checked and removed on every success, error, and handled-signal path. + +The existing test can exercise the systemd mount, device, UID, cgroup, and namespace properties only as a transient unit. Stage 3 therefore treats one activation-UUID-named `systemd-run --service-type=exec --wait --collect` service—and only its short-lived UID-1200 test process tree—as the narrow acceptance-test exception to “start only the namespace service” and the otherwise-inert UID. It is never installed, enabled, or persistent; its name must be unclaimed, and the committed command statically fixes every containment property. At runtime an additional random token binds the exact transient fragment, user/group, namespace path, activation-specific description, environment, single ExecStart, and captured invocation to this launch. HUP/INT/TERM are deferred only across client PID/start-time/ownership capture and replayed immediately afterward; cleanup will signal that client only while its `/proc` start time still matches. Success requires captured ownership, while error/signal cleanup stops only that token-bound identity and requires three consecutive successful `not-found` observations before accepting collection. It revalidates the activation process's inherited exclusive lock, has a 120-second runtime ceiling, and must leave no UID-1200 or `codex`-named process before activation can succeed or rollback can begin. No user manager is started. + +Every unrelated baseline is compared after start and after testing. Every same-boot automatic or explicit runtime cleanup compares against the recorded pre-activation baselines before mutation and again after cleanup, in addition to a fresh immediate rollback before/after comparison. Changed-boot recovery also compares the recorded baselines before and after and normally fails closed if reboot changed a runtime identity. Any network, policy, containment, provenance, or baseline failure exits nonzero without starting the user manager. + +## DNS boundary + +The nftables policy restricts classic DNS on UDP/TCP port 53 to `9.9.9.9` and `149.112.112.112`, and the contained resolver file contains only those two servers. Because public TCP 443 is intentionally allowed, this L3/L4 policy cannot distinguish ordinary HTTPS from DNS-over-HTTPS to another public provider. Stage 3 therefore proves the enforceable classic-DNS boundary; it does not claim application-layer DoH prevention. + +## Explicit exclusions + +Activation never enables a unit, starts `user@1200.service`, enables lingering, starts rootless Docker or Codex, authenticates Codex, invokes Docker mutation APIs, loads an arbitrary nftables file, creates a published port, accesses container environments or secrets, clones/copies application material, changes host sysctls, or repairs/restarts production. Rootful Docker access is read-only and explicitly uses `unix:///run/docker.sock`; safe fingerprints exclude container environments. + +During review, run only `tests/53-stage3-static-tests.sh` and `tests/54-stage3-failure-path-tests.sh`. The latter uses isolated `/tmp` fixtures and source guards; it never invokes the activation, rollback, or inert operational entry points, nor any systemd, networking, nftables, or Docker runtime command. + +## Future operator invocations + +Only after this package has been reviewed, committed, and checked out as a clean worktree on TITANSERVER, run from the repository root: + +```sh +sudo /bin/bash docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh +``` + +If activation fails or Stage 3 must be removed, use the separately reviewed retryable rollback entry point: + +```sh +sudo /bin/bash docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh +``` diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/ROLLBACK.md b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/ROLLBACK.md new file mode 100644 index 0000000..2c13d7a --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/ROLLBACK.md @@ -0,0 +1,17 @@ +# Phase 2B Stage 3-only rollback and recovery + +This rollback leaves every Stage 1 file/state object and the Stage 2 systemd reload intact. It never invokes the Stage 1 rollback, reloads systemd, stops a production/rootful-Docker service, or invokes the installed namespace helper's broad `down` action directly. + +Rollback accepts exactly one authoritative record: the approval marker or the retained rollback tombstone. It rejects dual records, symlinks, malformed/duplicate/unknown fields, noncanonical delimiters or incomplete final lines, embedded NUL bytes, path-like or escaping values, wrong ownership/mode/link count, host or machine mismatch, conflicting temporary records, and repository provenance drift. An empty fixed temporary is recognized only as the pre-render interruption state; a complete temporary must share every immutable provenance/baseline field and activation ID, and a same-status record must be byte-identical. If a complete temporary is the only surviving record, it is fsynced and revalidated before atomic publication as the retained authority. A complete `ACTIVE` temporary may upgrade a `PREPARED` tombstone only after the temporary is fsynced and its inode/hash plus the retained tombstone are revalidated immediately before the atomic rename; the stricter recorded live identities are then rechecked before cleanup. It requires the same boot for runtime cleanup, pinned manifests/state/files/directories, the exact loaded unit definitions, no user manager/linger/UID-1200 or `codex`-named process/rootless socket, no inert transient, no namespace PID, and absent-or-exact project resources. + +Before cleanup, rollback proves the current public IP, nonproject nftables, host network, rootful Docker network/runtime/ports/listeners, and resolver state still match the baselines recorded before activation, then captures a fresh immediate rollback baseline. Typed inventories reject command errors and syntactically valid wrong-shape JSON rather than treating them as absence. Both fixed project veth names are excluded from the nonproject baseline, which permits a retry after interruption while the peer is still on the host. Docker reads scrub context/TLS selectors and use only the explicit rootful socket and committed empty client configuration. Only after all gates pass does one same-directory atomic rename move the validated marker to `.OPERATOR-INPUTS-APPROVED.stage3-rollback`. That rename simultaneously revokes approval and preserves the exact authoritative bytes; there is no copy/delete gap, an atomic temporary upgrade never leaves the path without an authority, and the tombstone is never removed by Stage 3. + +An inactive service or successful stop is not cleanup proof. Rollback stops only `le-app-codex-netns.service`; its verified `ExecStop` normally removes the project objects. It then requires successful global inventories. Each surviving table is rechecked immediately by name, handle, and its independent canonical digest; the host veth is rechecked by type, ifindex, iflink, MAC, address, and live peer relationship; the namespace is rechecked by list membership, non-symlink nsfs identity, recorded dev/inode, and a successful empty-PID query. Only the exact survivor is directly deleted, in table → veth → namespace order. Drift or an inventory error preserves the tombstone and every unproven survivor. + +An `ACTIVE` retry accepts a recorded resource that is already absent because an earlier cleanup step succeeded, but a present survivor must retain its recorded identity. A `PREPARED` crash recovery requires a current attributable service invocation while authority is still the live approval marker, plus an exact subset of the helper's ordered construction or cleanup states, including namespace-only, both peers still on the host, and the peer moved into the namespace. Once that authority has been atomically retained as the tombstone and the service has been stopped, a retry may accept inactive/dead state with an empty, garbage-collected invocation ID; it still requires the same retained provenance, exact survivor shapes/identities, and unchanged baselines. A presence-shape gate rejects impossible ordering before the per-resource checks; changed or unproven fixed-name objects are preserved. JSON rule semantics/order, table handles/digests, reciprocal veth peer indices, addresses/routes, and namespace membership are rechecked as applicable. A retry after any partial rollback reads the same retained tombstone and resumes only after all gates pass again. + +After cleanup, rollback proves the service is inactive/dead/static, the approval marker and every project resource are absent, the tombstone remains valid, Stage 1 remains exact and installed, the Stage 2-loaded fragment/drop-ins remain exact, and both the immediate and recorded nonproject/production baselines remain unchanged. It never repairs, restarts, or modifies production. The retained tombstone deliberately blocks accidental repeat activation; removing or archiving it belongs to a separately reviewed later stage. + +On a changed boot ID, ephemeral namespace state should already be gone because the service is static. Rollback still compares every recorded pre-activation baseline before and after its action; a normal reboot will often change runtime identities and therefore fail closed pending a separately reviewed recovery. Only if those recorded comparisons pass does rollback also require a successful typed-inventory proof that the service and every fixed runtime name are absent, no user/Codex runtime exists, and a fresh immediate production baseline can be captured. It then retains/promotes the authoritative tombstone without deleting any runtime object and proves both the immediate and recorded production baselines unchanged afterward. If any identity, resource, or baseline differs, deletion and tombstone mutation are refused. + +Activation's EXIT/signal handler invokes this same rollback under the validated inherited lock. The inert runner owns and cleans only its verified UUID-named transient invocation, defers handled signals through client identity capture, refuses to signal a reused client PID, and requires stable collection; rollback refuses to proceed while any such transient remains. `SIGKILL` or power loss can bypass shell handlers, but the transient has a finite runtime and the standalone rollback can resume from `PREPARED`, `ACTIVE`, or the retained tombstone. Do not use `phase2b-contained-runtime/tests/31-rollback-stage1.sh` after Stage 3 activation: its documented safety contract forbids use after a marker, namespace, or firewall component has ever been activated. diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh new file mode 100644 index 0000000..b529441 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C +export PATH=/usr/bin:/bin +export SYSTEMD_COLORS=0 +export SYSTEMD_LOG_COLOR=0 +export SYSTEMD_PAGER=cat + +failures=0 +cleanup_armed=0 +cleanup_running=0 +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } +source "$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/stage3-common.sh" +proxy_free_env=(/usr/bin/env -u ALL_PROXY -u HTTPS_PROXY -u HTTP_PROXY -u NO_PROXY -u all_proxy -u https_proxy -u http_proxy -u no_proxy) + +activation_cleanup() { + local original_rc=$? cleanup_rc=0 + trap - EXIT HUP INT TERM + if [ "$cleanup_armed" -eq 1 ] && [ "$cleanup_running" -eq 0 ]; then + cleanup_running=1 + printf 'Stage 3 activation failed; entering reviewed project-only rollback.\n' >&2 + PHASE2B_STAGE3_LOCK_HELD=1 PHASE2B_STAGE3_LOCK_ID="$PHASE2B_STAGE3_LOCK_ID" /bin/bash "$stage3_root/tests/51-rollback-stage3.sh" --activation-failure || cleanup_rc=1 + fi + if [ "$cleanup_rc" -ne 0 ]; then printf 'FAIL: automatic Stage 3 rollback requires operator review\n' >&2; fi + [ "$original_rc" -ne 0 ] || original_rc=1 + exit "$original_rc" +} + +stage3_abort_if_failed() { + if [ "$failures" -ne 0 ]; then printf 'Phase 2B Stage 3 activation failures=%s\n' "$failures" >&2; exit 1; fi +} + +stage3_verify_project_topology() { + local active sub result enabled service_show service_rc enabled_rc listeners listeners_rc + if service_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState --property=Result 2>/dev/null); then service_rc=0; else service_rc=$?; fi + active=$(stage3_show_value "$service_show" ActiveState 2>/dev/null || true) + sub=$(stage3_show_value "$service_show" SubState 2>/dev/null || true) + result=$(stage3_show_value "$service_show" Result 2>/dev/null || true) + if enabled=$(/usr/bin/systemctl is-enabled le-app-codex-netns.service 2>/dev/null); then enabled_rc=0; else enabled_rc=$?; fi + if [ "$service_rc" -eq 0 ] && { [ "$enabled_rc" -eq 0 ] || [ "$enabled_rc" -eq 1 ]; } && [ "$active" = active ] && [ "$sub" = exited ] && [ "$result" = success ] && [ "$enabled" = static ]; then pass 'namespace service active/exited/success/static'; else fail "namespace service query/state rc=$service_rc/$enabled_rc $active/$sub/$result/$enabled"; fi + if stage3_capture_active_topology_identity; then pass 'namespace/nsfs, exact links, reciprocal veth, addresses, routes, empty PID set, and IPv6 state'; else fail 'active namespace/veth topology query or identity'; fi + stage3_inventory_has_table inet le_app_codex && stage3_inventory_has_table ip le_app_codex_nat && pass 'both project nftables tables present in typed inventory' || fail 'project nftables table absence or inventory failure' + listeners=$(/usr/bin/ip netns exec le-app-codex /usr/bin/ss -H -lntup 2>/dev/null); listeners_rc=$? + [ "$listeners_rc" -eq 0 ] && [ -z "$listeners" ] && pass 'namespace has no listeners or published ports' || fail 'namespace listener exists or query failed' +} + +stage3_verify_project_nft() { + if stage3_capture_exact_project_policy_snapshot; then + pass 'project nftables policy has exact JSON structure/rules/sets and two stable canonical snapshots' + else + fail 'project nftables policy query, exactness, or stable-snapshot validation' + return 1 + fi +} + +stage3_rule_counter() { + local chain=$1 index=$2 raw + raw=$(/usr/bin/nft -j list chain inet le_app_codex "$chain" 2>/dev/null) || return 1 + printf '%s' "$raw" | /usr/bin/jq -er --argjson index "$index" '[.nftables[] | .rule? | select(.)] | .[$index].expr | ([.[] | .counter?.packets // empty] | add // 0)' +} + +stage3_expect_blocked_counter() { + local label=$1 chain=$2 index=$3 before after rc + shift 3 + before=$(stage3_rule_counter "$chain" "$index") || { fail "$label pre-counter unavailable"; return 1; } + "$@" >/dev/null 2>&1 + rc=$? + after=$(stage3_rule_counter "$chain" "$index") || { fail "$label post-counter unavailable"; return 1; } + if [ "$rc" -ne 0 ] && [[ "$before" =~ ^[0-9]+$ ]] && [[ "$after" =~ ^[0-9]+$ ]] && [ "$after" -gt "$before" ]; then pass "$label denied with counter delta $before->$after"; else fail "$label denial/counter rc=$rc $before->$after"; return 1; fi +} + +stage3_positive_dns() { + local server=$1 transport=$2 output args=() + [ "$transport" = tcp ] && args+=(+tcp) + output=$(/usr/bin/timeout 10 /usr/bin/ip netns exec le-app-codex /usr/bin/dig +time=3 +tries=2 +short "${args[@]}" "@$server" example.com A 2>/dev/null) + if [ $? -eq 0 ] && printf '%s\n' "$output" | /usr/bin/awk '/^[0-9]+([.][0-9]+){3}$/ { found=1 } END { exit !found }'; then pass "Quad9 $server $transport DNS"; else fail "Quad9 $server $transport DNS"; return 1; fi +} + +stage3_network_matrix() { + local gateway reserved web_ipv4 current_docker + current_docker=$(stage3_docker_network_canonical) || { fail 'Docker gateway inventory query failed'; return 1; } + stage3_materialize_docker_ipv4_gateways "$current_docker" || { fail 'Docker gateway inventory is not exact 39-address set'; return 1; } + stage3_positive_dns 9.9.9.9 udp || return 1 + stage3_positive_dns 9.9.9.9 tcp || return 1 + stage3_positive_dns 149.112.112.112 udp || return 1 + stage3_positive_dns 149.112.112.112 tcp || return 1 + stage3_expect_blocked_counter 'non-Quad9 UDP DNS' forward 9 /usr/bin/timeout 6 /usr/bin/ip netns exec le-app-codex /usr/bin/dig +time=2 +tries=1 @1.1.1.1 example.com A || return 1 + stage3_expect_blocked_counter 'non-Quad9 TCP DNS' forward 9 /usr/bin/timeout 6 /usr/bin/ip netns exec le-app-codex /usr/bin/dig +tcp +time=2 +tries=1 @1.1.1.1 example.com A || return 1 + web_ipv4=$(/usr/bin/timeout 10 /usr/bin/ip netns exec le-app-codex /usr/bin/dig +time=3 +tries=2 +short @9.9.9.9 example.com A 2>/dev/null | /usr/bin/awk '/^[0-9]+([.][0-9]+){3}$/ { print; exit }') + case "$web_ipv4" in ''|0.*|10.*|100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*|127.*|169.254.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*|192.168.*|198.18.*|198.19.*|224.*|22[5-9].*|23[0-9].*|24[0-9].*|25[0-5].*|74.67.173.56) fail "unsafe public web test address ${web_ipv4:-missing}"; return 1 ;; esac + /usr/bin/timeout 20 /usr/bin/ip netns exec le-app-codex "${proxy_free_env[@]}" /usr/bin/curl --noproxy '*' -4sS --connect-timeout 5 --max-time 15 --resolve "example.com:80:$web_ipv4" -o /dev/null http://example.com/ && pass 'public TCP/80 works' || { fail 'public TCP/80'; return 1; } + /usr/bin/timeout 20 /usr/bin/ip netns exec le-app-codex "${proxy_free_env[@]}" /usr/bin/curl --noproxy '*' -4fsS --connect-timeout 5 --max-time 15 --resolve "example.com:443:$web_ipv4" -o /dev/null https://example.com/ && pass 'public TCP/443 works' || { fail 'public TCP/443'; return 1; } + stage3_expect_blocked_counter 'namespace peer/host denied' input 3 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 10.200.120.1 22 || return 1 + stage3_expect_blocked_counter 'host LAN address denied' input 3 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 192.168.10.151 22 || return 1 + stage3_expect_blocked_counter 'host WireGuard address denied' input 3 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 10.98.0.2 22 || return 1 + for gateway in "${stage3_docker_gateways[@]}"; do stage3_expect_blocked_counter "Docker gateway $gateway denied" input 3 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z "$gateway" 80 || return 1; done + stage3_expect_blocked_counter 'LAN gateway denied' forward 5 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 192.168.10.1 53 || return 1 + stage3_expect_blocked_counter 'private remote denied' forward 5 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 10.123.45.67 80 || return 1 + stage3_expect_blocked_counter 'metadata denied' forward 5 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex "${proxy_free_env[@]}" /usr/bin/curl --noproxy '*' -4sS --connect-timeout 2 --max-time 4 http://169.254.169.254/latest/meta-data/ || return 1 + for reserved in 100.64.0.1 192.0.0.1 192.0.2.1 192.88.99.1 198.18.0.1 198.51.100.1 203.0.113.1 240.0.0.1; do stage3_expect_blocked_counter "reserved $reserved denied" forward 5 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z "$reserved" 80 || return 1; done + /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 127.0.0.1 80 >/dev/null 2>&1 && { fail 'namespace loopback unexpectedly reachable'; return 1; } || pass 'isolated namespace loopback denied/no listener' + stage3_expect_blocked_counter 'observed public/NAT IPv4 denied' forward 4 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z "$expected_public_ipv4" 80 || return 1 + stage3_expect_blocked_counter 'SMTP/25 denied' forward 7 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 1.1.1.1 25 || return 1 + stage3_expect_blocked_counter 'SMTP/465 denied' forward 7 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 1.1.1.1 465 || return 1 + stage3_expect_blocked_counter 'SMTP/587 denied' forward 7 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 1.1.1.1 587 || return 1 + stage3_expect_blocked_counter 'other public TCP port denied' forward 9 /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex /usr/bin/nc -w2 -z 1.1.1.1 22 || return 1 + stage3_expect_blocked_counter 'public UDP/443 denied' forward 9 /usr/bin/timeout 6 /usr/bin/ip netns exec le-app-codex /usr/bin/dig +time=2 +tries=1 -p 443 @1.1.1.1 example.com A || return 1 + /usr/bin/timeout 5 /usr/bin/ip netns exec le-app-codex "${proxy_free_env[@]}" /usr/bin/curl --noproxy '*' -6sS --connect-timeout 2 --max-time 4 'https://[2606:4700:4700::1111]/' >/dev/null 2>&1 && { fail 'IPv6 connection unexpectedly succeeded'; return 1; } || pass 'IPv6 connection denied' + stage3_capture_exact_project_policy_snapshot || { fail 'project policy exact snapshot query failed after probes'; return 1; } + [ "$stage3_snapshot_inet_handle" = "$stage3_nft_inet_handle" ] && + [ "$stage3_snapshot_nat_handle" = "$stage3_nft_nat_handle" ] && + [ "$stage3_snapshot_inet_sha256" = "$stage3_nft_inet_sha256" ] && + [ "$stage3_snapshot_nat_sha256" = "$stage3_nft_nat_sha256" ] && + [ "$stage3_snapshot_project_sha256" = "$stage3_project_nft_sha256" ] && + pass 'project policy exact stable snapshot unchanged after probes' || { fail 'project policy changed during probes'; return 1; } +} + +stage3_activation_main() { + local command pids pids_rc + trap activation_cleanup EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM + + for command in /bin/bash /usr/bin/awk /usr/bin/cat /usr/bin/chmod /usr/bin/chown /usr/bin/cut /usr/bin/date /usr/bin/docker /usr/bin/dig /usr/bin/env /usr/bin/flock /usr/bin/git /usr/bin/grep /usr/bin/hostname /usr/bin/id /usr/bin/install /usr/bin/ip /usr/bin/jq /usr/bin/kill /usr/bin/mv /usr/bin/nc /usr/bin/nft /usr/bin/pgrep /usr/bin/ps /usr/bin/sha256sum /usr/bin/sleep /usr/bin/sort /usr/bin/ss /usr/bin/stat /usr/bin/sync /usr/bin/sysctl /usr/bin/systemctl /usr/bin/systemd-analyze /usr/bin/systemd-run /usr/bin/timeout /usr/bin/unlink /usr/bin/wc; do [ -x "$command" ] || fail "required command missing $command"; done + [ "$(/usr/bin/id -u)" = 0 ] && pass 'running as root' || fail 'must run as root' + stage3_verify_lock_parent && pass 'lock/marker parent exact root-owned directory' || fail 'lock/marker parent metadata' + stage3_acquire_lock && pass 'exclusive Stage 3 lock acquired' || fail 'another Stage 3 operation holds the lock' + stage3_verify_repo_provenance || true + stage3_verify_stage1_state_and_files || true + [ "$(/usr/bin/sha256sum "$installed_policy" 2>/dev/null | /usr/bin/cut -d' ' -f1)" = "$expected_policy_digest" ] && pass 'installed policy digest pinned' || fail 'installed policy digest' + /usr/bin/nft --check -f "$installed_policy" && pass 'installed policy root syntax check without load' || fail 'installed policy root syntax check' + stage3_verify_pre_activation_inert || true + stage3_capture_baseline || true + stage3_verify_pre_activation_inert || true + stage3_verify_loaded_units inactive || true + stage3_abort_if_failed + + stage3_activation_id=$(< /proc/sys/kernel/random/uuid) + stage3_machine_id_sha256=$(/usr/bin/sha256sum /etc/machine-id | /usr/bin/cut -d' ' -f1) + stage3_timestamp=$(/usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ) + stage3_hostname=$(/usr/bin/hostname) + stage3_boot_id=$(< /proc/sys/kernel/random/boot_id) + cleanup_armed=1 + stage3_write_marker PREPARED && pass 'root-owned PREPARED approval marker published atomically' || { fail 'approval marker publication'; stage3_abort_if_failed; } + + /usr/bin/systemctl start le-app-codex-netns.service && pass 'started only persistent Stage 3 namespace service' || { fail 'namespace service start'; stage3_abort_if_failed; } + stage3_verify_project_topology + stage3_verify_project_nft + stage3_verify_no_user_runtime 'post-start' || true + stage3_compare_baseline 'post-start' || true + stage3_abort_if_failed + + stage3_capture_active_topology_identity && pass 'active topology identity recaptured immediately before provenance publication' || fail 'active topology recapture before provenance publication' + stage3_verify_project_nft + stage3_service_invocation_id=$(/usr/bin/systemctl show le-app-codex-netns.service --property=InvocationID --value 2>/dev/null) || fail 'namespace service invocation query' + [[ "$stage3_service_invocation_id" =~ ^[0-9a-f]{32}$ ]] || fail 'namespace service invocation identity' + stage3_namespace_dev_inode=$stage3_active_namespace_dev_inode + stage3_host_veth_ifindex=$stage3_active_host_ifindex + stage3_host_veth_iflink=$stage3_active_host_iflink + stage3_host_veth_mac=$stage3_active_host_mac + stage3_capture_exact_project_policy_snapshot || fail 'exact stable project policy snapshot immediately before ACTIVE publication' + stage3_nft_inet_handle=$stage3_snapshot_inet_handle + stage3_nft_nat_handle=$stage3_snapshot_nat_handle + stage3_nft_inet_sha256=$stage3_snapshot_inet_sha256 + stage3_nft_nat_sha256=$stage3_snapshot_nat_sha256 + stage3_project_nft_sha256=$stage3_snapshot_project_sha256 + stage3_abort_if_failed + stage3_write_marker ACTIVE && pass 'root-owned ACTIVE approval marker published atomically' || { fail 'ACTIVE marker publication'; stage3_abort_if_failed; } + + stage3_network_matrix || { fail 'Stage 3 network matrix returned failure'; stage3_abort_if_failed; } + PHASE2B_STAGE3_LOCK_HELD=1 PHASE2B_STAGE3_LOCK_ID="$PHASE2B_STAGE3_LOCK_ID" /bin/bash "$stage3_root/tests/52-run-stage1-inert.sh" && pass 'existing installed Stage 1 inert containment test' || { fail 'existing Stage 1 inert containment test'; stage3_abort_if_failed; } + + stage3_verify_no_user_runtime 'post-tests' || true + pids=$(/usr/bin/ip netns pids le-app-codex 2>/dev/null); pids_rc=$? + [ "$pids_rc" -eq 0 ] && [ -z "$pids" ] && pass 'namespace empty after transient test' || fail 'namespace PID query failed or retains a process after transient test' + [ ! -e "$inert_probe" ] && [ ! -L "$inert_probe" ] && pass 'inert write probe cleaned' || fail 'inert write probe residue' + stage3_read_marker && [ "${stage3_marker_fields[STATUS]}" = ACTIVE ] && [ "${stage3_marker_fields[STAGE3_ACTIVATION_COMMIT]}" = "$stage3_head" ] && pass 'ACTIVE approval marker reverified' || fail 'ACTIVE approval marker drift' + stage3_compare_baseline 'post-tests' || true + stage3_verify_project_topology + stage3_capture_exact_project_policy_snapshot && + [ "$stage3_snapshot_inet_handle" = "${stage3_marker_fields[NFT_INET_HANDLE]:-}" ] && + [ "$stage3_snapshot_nat_handle" = "${stage3_marker_fields[NFT_NAT_HANDLE]:-}" ] && + [ "$stage3_snapshot_inet_sha256" = "${stage3_marker_fields[NFT_INET_SHA256]:-}" ] && + [ "$stage3_snapshot_nat_sha256" = "${stage3_marker_fields[NFT_NAT_SHA256]:-}" ] && + [ "$stage3_snapshot_project_sha256" = "${stage3_marker_fields[PROJECT_NFT_SHA256]:-}" ] && + pass 'final project nftables policy exactly matches retained stable snapshot' || fail 'final project nftables policy drift, instability, or query failure' + stage3_abort_if_failed + + cleanup_armed=0 + trap - EXIT HUP INT TERM + printf 'Phase 2B Stage 3 activation marker=%s service=active namespace=le-app-codex failures=0\n' "$marker" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_activation_main "$@"; fi diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh new file mode 100644 index 0000000..a86207e --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C +export PATH=/usr/bin:/bin +export SYSTEMD_COLORS=0 +export SYSTEMD_LOG_COLOR=0 +export SYSTEMD_PAGER=cat + +failures=0 +activation_failure=0 +record_location= +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } +source "$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/stage3-common.sh" + +stage3_resources_absent() { + stage3_capture_resource_inventory || return 2 + if stage3_inventory_has_namespace || stage3_inventory_has_link || stage3_inventory_has_host_ns_peer || stage3_inventory_has_table inet le_app_codex || stage3_inventory_has_table ip le_app_codex_nat || [ -e /run/netns/le-app-codex ] || [ -L /run/netns/le-app-codex ]; then return 1; fi + return 0 +} + +stage3_prepared_table_shape() { + local family=$1 name=$2 raw text + raw=$(stage3_query_nft_table_json "$family" "$name") || return 1 + text=$(stage3_query_nft_table_text "$family" "$name") || return 1 + if [ "$family/$name" = inet/le_app_codex ]; then + stage3_project_inet_snapshot_valid "$raw" "$text" + else + [ "$family/$name" = ip/le_app_codex_nat ] || return 1 + stage3_project_nat_snapshot_valid "$raw" "$text" + fi +} + +stage3_table_identity() { + local family=$1 name=$2 expected_handle expected_hash actual_handle actual_hash + stage3_inventory_has_table "$family" "$name" || return 0 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + if [ "$family" = inet ]; then expected_handle=${stage3_marker_fields[NFT_INET_HANDLE]}; expected_hash=${stage3_marker_fields[NFT_INET_SHA256]}; else expected_handle=${stage3_marker_fields[NFT_NAT_HANDLE]}; expected_hash=${stage3_marker_fields[NFT_NAT_SHA256]}; fi + actual_handle=$(stage3_inventory_table_handle "$family" "$name") || return 1 + actual_hash=$(stage3_nft_table_digest "$family" "$name") || return 1 + [ "$actual_handle" = "$expected_handle" ] && [ "$actual_hash" = "$expected_hash" ] + else + stage3_prepared_table_shape "$family" "$name" + fi +} + +stage3_namespace_identity() { + local pids pids_rc current ns_json + stage3_inventory_has_namespace || { [ ! -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ]; return; } + [ -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ] || return 1 + [ "$(/usr/bin/stat -f -c '%T' /run/netns/le-app-codex 2>/dev/null)" = nsfs ] || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + current=$(/usr/bin/stat -Lc '%D:%i' /run/netns/le-app-codex 2>/dev/null) || return 1 + [ "$current" = "${stage3_marker_fields[NAMESPACE_DEV_INODE]}" ] || return 1 + fi + pids=$(/usr/bin/ip netns pids le-app-codex 2>/dev/null); pids_rc=$? + [ "$pids_rc" -eq 0 ] && [ -z "$pids" ] || return 1 + ns_json=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + printf '%s' "$ns_json" | /usr/bin/jq -e 'any(.[]; .ifname == "lo") and ([.[] | select(.ifname == "lecodex-ns")] | length) <= 1 and ([.[] | select(.ifname != "lo" and .ifname != "lecodex-ns")] | length) == 0' >/dev/null +} + +stage3_veth_identity() { + local link_json addr_json host_ipv6 current_ifindex current_iflink current_mac peer_json peer_addr_json peer_ipv6 peer_ifindex peer_iflink peer_location= ns_links ns_routes4 ns_routes6 ns_ipv6 + if ! stage3_inventory_has_link; then + ! stage3_inventory_has_host_ns_peer || return 1 + if stage3_inventory_has_namespace; then + ns_links=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + printf '%s' "$ns_links" | /usr/bin/jq -e 'all(.[]; .ifname != "lecodex-ns")' >/dev/null || return 1 + fi + return 0 + fi + link_json=$(printf '%s' "$stage3_inventory_links" | /usr/bin/jq -ec '[.[] | select(.ifname == "lecodex-host")] | select(length == 1)') || return 1 + printf '%s' "$link_json" | /usr/bin/jq -e '.[0].linkinfo.info_kind == "veth"' >/dev/null || return 1 + current_ifindex=$(< /sys/class/net/lecodex-host/ifindex) || return 1 + current_iflink=$(< /sys/class/net/lecodex-host/iflink) || return 1 + current_mac=$(< /sys/class/net/lecodex-host/address) || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + stage3_veth_fields_match "${stage3_marker_fields[HOST_VETH_IFINDEX]}" "${stage3_marker_fields[HOST_VETH_IFLINK]}" "${stage3_marker_fields[HOST_VETH_MAC]}" "$current_ifindex" "$current_iflink" "$current_mac" || return 1 + fi + addr_json=$(/usr/bin/ip -j -4 address show dev lecodex-host 2>/dev/null) || return 1 + host_ipv6=$(/usr/bin/ip -j -6 address show dev lecodex-host 2>/dev/null) || return 1 + printf '%s' "$host_ipv6" | /usr/bin/jq -e 'all(.[].addr_info[]?; .family != "inet6" or .scope == "link")' >/dev/null || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + printf '%s' "$addr_json" | /usr/bin/jq -e '[.[].addr_info[] | select(.family == "inet") | {local,prefixlen}] == [{"local":"10.200.120.1","prefixlen":30}]' >/dev/null || return 1 + else + printf '%s' "$addr_json" | /usr/bin/jq -e '([.[].addr_info[] | select(.family == "inet") | {local,prefixlen}]) as $a | ($a == [] or $a == [{"local":"10.200.120.1","prefixlen":30}])' >/dev/null || return 1 + fi + if stage3_inventory_has_host_ns_peer; then + peer_location=host + peer_json=$(printf '%s' "$stage3_inventory_links" | /usr/bin/jq -ec '[.[] | select(.ifname == "lecodex-ns")] | select(length == 1)') || return 1 + peer_ifindex=$(< /sys/class/net/lecodex-ns/ifindex) || return 1 + peer_iflink=$(< /sys/class/net/lecodex-ns/iflink) || return 1 + peer_addr_json=$(/usr/bin/ip -j -4 address show dev lecodex-ns 2>/dev/null) || return 1 + peer_ipv6=$(/usr/bin/ip -j -6 address show dev lecodex-ns 2>/dev/null) || return 1 + printf '%s' "$peer_ipv6" | /usr/bin/jq -e 'all(.[].addr_info[]?; .family != "inet6" or .scope == "link")' >/dev/null || return 1 + if stage3_inventory_has_namespace; then + ns_links=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + printf '%s' "$ns_links" | /usr/bin/jq -e 'all(.[]; .ifname != "lecodex-ns")' >/dev/null || return 1 + fi + elif stage3_inventory_has_namespace; then + peer_location=namespace + ns_links=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + peer_json=$(printf '%s' "$ns_links" | /usr/bin/jq -ec '[.[] | select(.ifname == "lecodex-ns")] | select(length == 1)') || return 1 + peer_ifindex=$(/usr/bin/ip netns exec le-app-codex /usr/bin/cat /sys/class/net/lecodex-ns/ifindex 2>/dev/null) || return 1 + peer_iflink=$(/usr/bin/ip netns exec le-app-codex /usr/bin/cat /sys/class/net/lecodex-ns/iflink 2>/dev/null) || return 1 + peer_addr_json=$(/usr/bin/ip -n le-app-codex -j -4 address show dev lecodex-ns 2>/dev/null) || return 1 + else + return 1 + fi + printf '%s' "$peer_json" | /usr/bin/jq -e 'length == 1 and .[0].ifname == "lecodex-ns" and .[0].linkinfo.info_kind == "veth"' >/dev/null || return 1 + [ "$peer_ifindex" = "$current_iflink" ] && [ "$peer_iflink" = "$current_ifindex" ] || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ] && [ "$peer_location" = namespace ]; then + printf '%s' "$peer_addr_json" | /usr/bin/jq -e '[.[].addr_info[] | select(.family == "inet") | {local,prefixlen}] == [{"local":"10.200.120.2","prefixlen":30}]' >/dev/null || return 1 + else + printf '%s' "$peer_addr_json" | /usr/bin/jq -e '([.[].addr_info[] | select(.family == "inet") | {local,prefixlen}]) as $a | ($a == [] or $a == [{"local":"10.200.120.2","prefixlen":30}])' >/dev/null || return 1 + fi + if [ "$peer_location" = namespace ]; then + ns_routes4=$(/usr/bin/ip -n le-app-codex -j -4 route show table main 2>/dev/null) || return 1 + ns_ipv6=$(/usr/bin/ip -n le-app-codex -j -6 address show 2>/dev/null) || return 1 + ns_routes6=$(/usr/bin/ip -n le-app-codex -j -6 route show table main 2>/dev/null) || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + printf '%s' "$ns_routes4" | /usr/bin/jq -e 'length == 2 and any(.[]; .dst == "10.200.120.0/30" and .dev == "lecodex-ns") and any(.[]; .dst == "default" and .gateway == "10.200.120.1" and .dev == "lecodex-ns")' >/dev/null || return 1 + printf '%s' "$ns_ipv6" | /usr/bin/jq -e '[.[].addr_info[]? | select(.family == "inet6")] | length == 0' >/dev/null || return 1 + printf '%s' "$ns_routes6" | /usr/bin/jq -e 'length == 0' >/dev/null || return 1 + else + printf '%s' "$ns_routes4" | /usr/bin/jq -e 'length <= 2 and all(.[]; (.dst == "10.200.120.0/30" and .dev == "lecodex-ns") or (.dst == "default" and .gateway == "10.200.120.1" and .dev == "lecodex-ns"))' >/dev/null || return 1 + printf '%s' "$ns_ipv6" | /usr/bin/jq -e 'all(.[].addr_info[]?; .family != "inet6" or .scope == "link" or .local == "::1")' >/dev/null || return 1 + printf '%s' "$ns_routes6" | /usr/bin/jq -e 'all(.[]; ((.dst // "") | startswith("fe80:")) or .dst == "::1")' >/dev/null || return 1 + fi + fi +} + +stage3_service_identity() { + local show active sub invocation + show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState --property=InvocationID 2>/dev/null) || return 1 + active=$(stage3_show_value "$show" ActiveState) || return 1 + sub=$(stage3_show_value "$show" SubState) || return 1 + invocation=$(stage3_show_value "$show" InvocationID) || return 1 + if [ "$active/$sub" = inactive/dead ]; then return 0; fi + [ "$active/$sub" = active/exited ] || [ "$active/$sub" = failed/failed ] || return 1 + [[ "$invocation" =~ ^[0-9a-f]{32}$ ]] || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then [ "$invocation" = "${stage3_marker_fields[SERVICE_INVOCATION_ID]}" ]; else return 0; fi +} + +stage3_resource_presence_shape_valid() { + local namespace=$1 host=$2 peer_host=$3 peer_namespace=$4 inet_table=$5 nat_table=$6 value + for value in "$namespace" "$host" "$peer_host" "$peer_namespace" "$inet_table" "$nat_table"; do [ "$value" = 0 ] || [ "$value" = 1 ] || return 1; done + if [ "$namespace" -eq 0 ]; then [ "$host$peer_host$peer_namespace$inet_table$nat_table" = 00000 ]; return; fi + if [ "$host" -eq 0 ]; then [ "$peer_host$peer_namespace$inet_table$nat_table" = 0000 ]; return; fi + [ $((peer_host + peer_namespace)) -eq 1 ] || return 1 + if [ "$peer_host" -eq 1 ]; then [ "$inet_table$nat_table" = 00 ]; return; fi + return 0 +} + +stage3_prepared_live_invocation_valid() { + local has_resources=$1 invocation=${2:-} + [ "$has_resources" = 0 ] || [ "$has_resources" = 1 ] || return 1 + [ "${stage3_marker_fields[STATUS]}" = PREPARED ] || return 0 + [ "$has_resources" -eq 1 ] || return 0 + if [ "$record_location" = tombstone ]; then return 0; fi + [ "$record_location" = marker ] && [[ "$invocation" =~ ^[0-9a-f]{32}$ ]] +} + +stage3_verify_survivors() { + local namespace_present=0 host_present=0 peer_host_present=0 peer_namespace_present=0 inet_present=0 nat_present=0 ns_links has_resources=0 invocation= + stage3_capture_resource_inventory || return 1 + stage3_inventory_has_namespace && namespace_present=1 + stage3_inventory_has_link && host_present=1 + stage3_inventory_has_host_ns_peer && peer_host_present=1 + stage3_inventory_has_table inet le_app_codex && inet_present=1 + stage3_inventory_has_table ip le_app_codex_nat && nat_present=1 + if [ "$namespace_present" -eq 1 ]; then + ns_links=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + printf '%s' "$ns_links" | /usr/bin/jq -e 'type == "array" and all(.[]; type == "object" and (.ifname | type) == "string")' >/dev/null || return 1 + printf '%s' "$ns_links" | /usr/bin/jq -e 'any(.[]; .ifname == "lecodex-ns")' >/dev/null && peer_namespace_present=1 + fi + stage3_resource_presence_shape_valid "$namespace_present" "$host_present" "$peer_host_present" "$peer_namespace_present" "$inet_present" "$nat_present" || return 1 + stage3_service_identity || return 1 + stage3_table_identity inet le_app_codex || return 1 + stage3_table_identity ip le_app_codex_nat || return 1 + stage3_veth_identity || return 1 + stage3_namespace_identity || return 1 + if [ "$namespace_present" -eq 1 ] || [ "$host_present" -eq 1 ] || [ "$inet_present" -eq 1 ] || [ "$nat_present" -eq 1 ]; then has_resources=1; fi + if [ "${stage3_marker_fields[STATUS]}" = PREPARED ] && [ "$has_resources" -eq 1 ] && [ "$record_location" = marker ]; then + invocation=$(/usr/bin/systemctl show le-app-codex-netns.service --property=InvocationID --value 2>/dev/null) || return 1 + fi + stage3_prepared_live_invocation_valid "$has_resources" "$invocation" +} + +stage3_validate_safe_temporary() { + local temporary=$1 authority=$2 expected_uid=${3:-0} expected_gid=${4:-0} require_provenance=${5:-1} stat stat_uid stat_gid stat_mode links size type activation record_status record_common record_hash temp_activation temp_status temp_common temp_hash + [ -e "$temporary" ] || [ -L "$temporary" ] || return 0 + stat=$(/usr/bin/stat -c '%u|%g|%a|%h|%s|%F' "$temporary" 2>/dev/null) || return 1 + IFS='|' read -r stat_uid stat_gid stat_mode links size type <<< "$stat" + [ "$stat_uid" = "$expected_uid" ] && [ "$stat_gid" = "$expected_gid" ] && [ "$stat_mode" = 600 ] && [ "$links" = 1 ] && { [ "$type" = 'regular file' ] || [ "$type" = 'regular empty file' ]; } && [ -f "$temporary" ] && [ ! -L "$temporary" ] || return 1 + [[ "$size" =~ ^[0-9]+$ ]] || return 1 + activation=${stage3_marker_fields[ACTIVATION_ID]} + record_status=${stage3_marker_fields[STATUS]} + record_common=$(stage3_record_common_sha256) || return 1 + record_hash=$(/usr/bin/sha256sum "$authority" 2>/dev/null | /usr/bin/cut -d' ' -f1) || return 1 + [ "$size" -ne 0 ] || return 0 + if ! stage3_parse_record_file "$temporary" "$require_provenance"; then + if [ "$authority" = "$marker" ]; then stage3_read_marker >/dev/null 2>&1 || true; else stage3_read_rollback_record >/dev/null 2>&1 || true; fi + return 1 + fi + temp_activation=${stage3_marker_fields[ACTIVATION_ID]} + temp_status=${stage3_marker_fields[STATUS]} + temp_common=$(stage3_record_common_sha256) || { if [ "$authority" = "$marker" ]; then stage3_read_marker >/dev/null 2>&1 || true; else stage3_read_rollback_record >/dev/null 2>&1 || true; fi; return 1; } + temp_hash=$(/usr/bin/sha256sum "$temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1) || { if [ "$authority" = "$marker" ]; then stage3_read_marker >/dev/null 2>&1 || true; else stage3_read_rollback_record >/dev/null 2>&1 || true; fi; return 1; } + if [ "$authority" = "$marker" ]; then stage3_read_marker || return 1; else stage3_read_rollback_record || return 1; fi + [ "$temp_activation" = "$activation" ] || return 1 + [ "$record_common" = "$temp_common" ] || return 1 + if [ "$record_status/$temp_status" = PREPARED/ACTIVE ]; then + return 0 + elif [ "$record_status" = "$temp_status" ]; then + [ "$record_hash" = "$temp_hash" ] + else + return 1 + fi +} + +stage3_cleanup_safe_temporary() { + local temporary=$1 expected_uid=${2:-0} expected_gid=${3:-0} require_provenance=${4:-1} stat size record_status temp_status temporary_id temporary_hash authority_id authority_hash + [ -e "$temporary" ] || [ -L "$temporary" ] || return 0 + stage3_validate_safe_temporary "$temporary" "$rollback_record" "$expected_uid" "$expected_gid" "$require_provenance" || return 1 + stat=$(/usr/bin/stat -c '%s' "$temporary" 2>/dev/null) || return 1 + [[ "$stat" =~ ^[0-9]+$ ]] || return 1 + size=$stat + if [ "$size" -eq 0 ]; then + /usr/bin/unlink -- "$temporary" && stage3_sync_parent "$temporary" + return + fi + record_status=${stage3_marker_fields[STATUS]} + stage3_parse_record_file "$temporary" "$require_provenance" || return 1 + temp_status=${stage3_marker_fields[STATUS]} + stage3_read_rollback_record || return 1 + if [ "$record_status/$temp_status" = PREPARED/ACTIVE ]; then + temporary_id=$(/usr/bin/stat -c '%D:%i' "$temporary" 2>/dev/null) || return 1 + temporary_hash=$(/usr/bin/sha256sum "$temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1) || return 1 + authority_id=$(/usr/bin/stat -c '%D:%i' "$rollback_record" 2>/dev/null) || return 1 + authority_hash=$(/usr/bin/sha256sum "$rollback_record" 2>/dev/null | /usr/bin/cut -d' ' -f1) || return 1 + /usr/bin/sync -f "$temporary" 2>/dev/null || /usr/bin/sync || return 1 + [ "$temporary_id" = "$(/usr/bin/stat -c '%D:%i' "$temporary" 2>/dev/null)" ] && [ "$temporary_hash" = "$(/usr/bin/sha256sum "$temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_validate_safe_temporary "$temporary" "$rollback_record" "$expected_uid" "$expected_gid" "$require_provenance" || return 1 + [ "$authority_id" = "$(/usr/bin/stat -c '%D:%i' "$rollback_record" 2>/dev/null)" ] && [ "$authority_hash" = "$(/usr/bin/sha256sum "$rollback_record" 2>/dev/null | /usr/bin/cut -d' ' -f1)" ] || return 1 + /usr/bin/mv -T -- "$temporary" "$rollback_record" || return 1 + [ "$(/usr/bin/stat -c '%D:%i' "$rollback_record" 2>/dev/null)" = "$temporary_id" ] || return 1 + stage3_sync_parent "$rollback_record" || return 1 + stage3_read_rollback_record && [ "${stage3_marker_fields[STATUS]}" = ACTIVE ] + return + fi + [ "$record_status" = "$temp_status" ] || return 1 + /usr/bin/unlink -- "$temporary" && stage3_sync_parent "$temporary" +} + +stage3_select_authoritative_record() { + local expected_uid=${1:-0} expected_gid=${2:-0} require_provenance=${3:-1} marker_exists=0 tomb_exists=0 temp_stat temp_uid temp_gid temp_mode temp_links temp_size temp_type temporary_id temporary_hash + if [ -e "$marker" ] || [ -L "$marker" ]; then marker_exists=1; fi + if [ -e "$rollback_record" ] || [ -L "$rollback_record" ]; then tomb_exists=1; fi + [ "$marker_exists" -eq 0 ] || [ "$tomb_exists" -eq 0 ] || return 1 + if [ "$tomb_exists" -eq 1 ]; then + stage3_read_rollback_record || return 1 + record_location=tombstone + elif [ "$marker_exists" -eq 1 ]; then + stage3_read_marker || return 1 + record_location=marker + elif [ -e "$marker_temporary" ] || [ -L "$marker_temporary" ]; then + temp_stat=$(/usr/bin/stat -c '%u|%g|%a|%h|%s|%F' "$marker_temporary" 2>/dev/null) || return 1 + IFS='|' read -r temp_uid temp_gid temp_mode temp_links temp_size temp_type <<< "$temp_stat" + [ "$temp_uid" = "$expected_uid" ] && [ "$temp_gid" = "$expected_gid" ] && [ "$temp_mode" = 600 ] && [ "$temp_links" = 1 ] && { [ "$temp_type" = 'regular file' ] || [ "$temp_type" = 'regular empty file' ]; } && [ -f "$marker_temporary" ] && [ ! -L "$marker_temporary" ] || return 1 + [[ "$temp_size" =~ ^[0-9]+$ ]] || return 1 + if ! stage3_parse_record_file "$marker_temporary" "$require_provenance"; then + [ "$temp_size" -eq 0 ] || return 1 + stage3_resources_absent || return 1 + /usr/bin/unlink -- "$marker_temporary" && stage3_sync_parent "$marker_temporary" || return 1 + return 3 + fi + temporary_id=$(/usr/bin/stat -c '%D:%i' "$marker_temporary" 2>/dev/null) || return 1 + temporary_hash=$(/usr/bin/sha256sum "$marker_temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1) || return 1 + /usr/bin/sync -f "$marker_temporary" 2>/dev/null || /usr/bin/sync || return 1 + [ "$temporary_id" = "$(/usr/bin/stat -c '%D:%i' "$marker_temporary" 2>/dev/null)" ] && [ "$temporary_hash" = "$(/usr/bin/sha256sum "$marker_temporary" 2>/dev/null | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_parse_record_file "$marker_temporary" "$require_provenance" || return 1 + stage3_atomic_publish_new "$marker_temporary" "$rollback_record" || return 1 + stage3_sync_parent "$rollback_record" || return 1 + stage3_read_rollback_record || return 1 + record_location=tombstone + else + return 3 + fi +} + +stage3_capture_rollback_baseline() { + rollback_before_public=$(stage3_public_ipv4) || return 1 + rollback_before_docker_network=$(stage3_docker_network_canonical | stage3_sha_stream) || return 1 + rollback_before_nft=$(stage3_nonproject_nft_canonical | stage3_sha_stream) || return 1 + rollback_before_host=$(stage3_host_network_canonical | stage3_sha_stream) || return 1 + rollback_before_docker_runtime=$(stage3_docker_runtime_canonical | stage3_sha_stream) || return 1 + [[ "$rollback_before_docker_network$rollback_before_nft$rollback_before_host$rollback_before_docker_runtime" =~ ^[0-9a-f]{256}$ ]] +} + +stage3_compare_rollback_baseline() { + [ "$(stage3_public_ipv4)" = "$rollback_before_public" ] || return 1 + [ "$(stage3_docker_network_canonical | stage3_sha_stream)" = "$rollback_before_docker_network" ] || return 1 + [ "$(stage3_nonproject_nft_canonical | stage3_sha_stream)" = "$rollback_before_nft" ] || return 1 + [ "$(stage3_host_network_canonical | stage3_sha_stream)" = "$rollback_before_host" ] || return 1 + [ "$(stage3_docker_runtime_canonical | stage3_sha_stream)" = "$rollback_before_docker_runtime" ] +} + +stage3_delete_surviving_table() { + local family=$1 name=$2 + stage3_capture_resource_inventory || return 1 + stage3_inventory_has_table "$family" "$name" || return 0 + stage3_table_identity "$family" "$name" || return 1 + if [ "$family/$name" = inet/le_app_codex ]; then /usr/bin/nft delete table inet le_app_codex; else /usr/bin/nft delete table ip le_app_codex_nat; fi +} + +stage3_delete_surviving_veth() { + stage3_capture_resource_inventory || return 1 + stage3_veth_identity || return 1 + stage3_inventory_has_link || return 0 + /usr/bin/ip link delete lecodex-host +} + +stage3_delete_surviving_namespace() { + stage3_capture_resource_inventory || return 1 + stage3_veth_identity || return 1 + ! stage3_inventory_has_link && ! stage3_inventory_has_host_ns_peer || return 1 + stage3_inventory_has_namespace || { [ ! -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ]; return; } + stage3_namespace_identity || return 1 + /usr/bin/ip netns delete le-app-codex +} + +stage3_rollback_main() { + local select_rc current_boot active sub enabled inert_units inert_rc before_gate authority_path service_show service_rc enabled_rc + [ "${1:-}" = --activation-failure ] && activation_failure=1 + if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ "$activation_failure" -ne 1 ]; }; then printf 'usage: %s [--activation-failure]\n' "$0" >&2; return 64; fi + + [ "$(/usr/bin/id -u)" = 0 ] && pass 'running as root' || fail 'must run as root' + stage3_verify_lock_parent && pass 'lock/marker parent exact' || fail 'lock/marker parent metadata' + stage3_acquire_lock && pass 'exclusive Stage 3 lock acquired' || fail 'another Stage 3 operation holds the lock' + stage3_verify_repo_provenance || true + stage3_verify_stage1_state_and_files || true + stage3_verify_loaded_units runtime || true + [ "$failures" -eq 0 ] || { printf 'Rollback preflight failed; no Stage 3 runtime object was changed.\n' >&2; return 1; } + + stage3_select_authoritative_record; select_rc=$? + if [ "$select_rc" -eq 3 ]; then + stage3_resources_absent; select_rc=$? + if [ "$activation_failure" -eq 1 ] && [ "$select_rc" -eq 0 ]; then stage3_cleanup_probe_exact "$inert_probe" || return 1; pass 'activation failed before authoritative publication and left no resources'; return 0; fi + fail 'no authoritative Stage 3 marker or tombstone' + return 1 + elif [ "$select_rc" -ne 0 ]; then + fail 'authoritative Stage 3 record state is unsafe or ambiguous' + return 1 + fi + pass "valid authoritative Stage 3 $record_location record" + if [ "$record_location" = marker ]; then authority_path=$marker; else authority_path=$rollback_record; fi + stage3_validate_safe_temporary "$marker_temporary" "$authority_path" || { fail 'approval temporary is unsafe, malformed, or mismatched'; return 1; } + + current_boot=$(< /proc/sys/kernel/random/boot_id) + if [ "$current_boot" != "${stage3_marker_fields[BOOT_ID]}" ]; then + stage3_resources_absent; select_rc=$? + if service_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState 2>/dev/null); then service_rc=0; else service_rc=$?; fi + active=$(stage3_show_value "$service_show" ActiveState 2>/dev/null || true) + sub=$(stage3_show_value "$service_show" SubState 2>/dev/null || true) + if [ "$select_rc" -eq 0 ] && [ "$service_rc" -eq 0 ] && [ "$active" = inactive ] && [ "$sub" = dead ]; then + before_gate=$failures + stage3_compare_recorded_baseline 'changed-boot pre-rollback' || true + stage3_verify_no_user_runtime 'changed-boot rollback' || true + stage3_capture_rollback_baseline || fail 'changed-boot immediate production baseline capture' + [ "$failures" -eq "$before_gate" ] || { printf 'Changed-boot rollback baseline/runtime gate failed; no Stage 3 state was changed.\n' >&2; return 1; } + if [ "$record_location" = marker ]; then stage3_promote_marker_to_tombstone || return 1; fi + stage3_cleanup_safe_temporary "$marker_temporary" || return 1 + stage3_compare_rollback_baseline || { fail 'changed-boot immediate production baseline drift'; return 1; } + stage3_compare_recorded_baseline 'changed-boot post-rollback' || true + stage3_verify_loaded_units inactive || true + stage3_verify_no_user_runtime 'changed-boot post-rollback' || true + [ "$failures" -eq "$before_gate" ] || return 1 + pass 'changed-boot record retained as tombstone after proving runtime resources absent' + return 0 + fi + fail 'boot changed and project resource absence cannot be proven' + return 1 + fi + + before_gate=$failures + stage3_compare_recorded_baseline 'pre-rollback' || true + stage3_verify_no_user_runtime 'pre-rollback' || true + inert_units=$(/usr/bin/systemctl list-units --all --plain --no-legend "$inert_unit_prefix*.service" 2>/dev/null); inert_rc=$? + [ "$inert_rc" -eq 0 ] && [ -z "$inert_units" ] || fail 'inert transient still exists or query failed' + stage3_verify_survivors || fail 'project survivor provenance/topology drift or inventory failure' + stage3_capture_rollback_baseline || fail 'immediate rollback baseline capture' + [ "$failures" -eq "$before_gate" ] || { printf 'Rollback provenance/baseline gate failed; no service or project resource was changed.\n' >&2; return 1; } + + if [ "$record_location" = marker ]; then stage3_promote_marker_to_tombstone && pass 'approval marker atomically promoted to retained rollback tombstone' || { fail 'rollback tombstone publication'; return 1; }; record_location=tombstone; fi + stage3_cleanup_safe_temporary "$marker_temporary" || { fail 'approval temporary cleanup'; return 1; } + stage3_verify_survivors || { fail 'project survivor changed after tombstone publication'; return 1; } + stage3_cleanup_probe_exact "$inert_probe" || { fail 'inert write-probe cleanup'; return 1; } + + if service_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState 2>/dev/null); then service_rc=0; else service_rc=$?; fi + [ "$service_rc" -eq 0 ] || { fail 'namespace service state query before stop'; return 1; } + active=$(stage3_show_value "$service_show" ActiveState 2>/dev/null) || { fail 'namespace service ActiveState before stop'; return 1; } + if [ "$active" != inactive ]; then /usr/bin/systemctl stop le-app-codex-netns.service && pass 'stopped only le-app-codex-netns.service' || fail 'namespace service stop'; else pass 'namespace service already inactive for retry'; fi + [ "$failures" -eq 0 ] || return 1 + + stage3_delete_surviving_table inet le_app_codex || { fail 'verified inet table cleanup'; return 1; } + stage3_delete_surviving_table ip le_app_codex_nat || { fail 'verified NAT table cleanup'; return 1; } + stage3_delete_surviving_veth || { fail 'verified host veth cleanup'; return 1; } + stage3_delete_surviving_namespace || { fail 'verified namespace cleanup'; return 1; } + + stage3_resources_absent; select_rc=$? + [ "$select_rc" -eq 0 ] && pass 'namespace, veth, and project tables absent via successful inventories' || fail "project absence result=$select_rc" + [ ! -e "$marker" ] && [ ! -L "$marker" ] && [ -f "$rollback_record" ] && [ ! -L "$rollback_record" ] && stage3_read_rollback_record && pass 'approval marker absent and authoritative tombstone retained' || fail 'marker/tombstone post-state' + if service_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState 2>/dev/null); then service_rc=0; else service_rc=$?; fi + active=$(stage3_show_value "$service_show" ActiveState 2>/dev/null || true) + sub=$(stage3_show_value "$service_show" SubState 2>/dev/null || true) + if enabled=$(/usr/bin/systemctl is-enabled le-app-codex-netns.service 2>/dev/null); then enabled_rc=0; else enabled_rc=$?; fi + [ "$service_rc" -eq 0 ] && { [ "$enabled_rc" -eq 0 ] || [ "$enabled_rc" -eq 1 ]; } && [ "$active" = inactive ] && [ "$sub" = dead ] && [ "$enabled" = static ] && pass 'namespace service inactive/dead/static' || fail "namespace service post-query/state rc=$service_rc/$enabled_rc $active/$sub/$enabled" + + stage3_compare_rollback_baseline && pass 'rollback changed no nonproject/production baseline' || fail 'rollback immediate production baseline drift' + stage3_compare_recorded_baseline 'post-rollback' || true + stage3_verify_stage1_state_and_files || true + stage3_verify_loaded_units inactive || true + stage3_verify_no_user_runtime 'post-rollback' || true + + printf 'Phase 2B Stage 3 rollback marker=absent tombstone=retained service=inactive namespace=absent stage1=installed failures=%s\n' "$failures" + [ "$failures" -eq 0 ] +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_rollback_main "$@"; fi diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/52-run-stage1-inert.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/52-run-stage1-inert.sh new file mode 100644 index 0000000..f2d62fa --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/52-run-stage1-inert.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash + +set -Eeu -o pipefail +export LC_ALL=C +export PATH=/usr/bin:/bin + +stage3_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd) +source "$stage3_root/tests/stage3-common.sh" +touch_wrapper=$stage3_root/tests/inert-bin/touch +rm_wrapper=$stage3_root/tests/inert-bin/rm +inert_client_pid= +inert_client_starttime= +inert_client_started=0 +inert_owned=0 +inert_invocation= +inert_activation= +inert_token= +inert_unit= +inert_pending_signal=0 +inert_probe_uid=1200 +inert_probe_gid=1200 +inert_probe_mode=644 + +stage3_inert_show() { + /usr/bin/systemctl show --no-pager "$1" --property=LoadState --property=ActiveState --property=Transient --property=FragmentPath --property=User --property=Group --property=NetworkNamespacePath --property=Description --property=Environment --property=UnsetEnvironment --property=ExecStart --property=InvocationID +} + +stage3_inert_stop() { /usr/bin/systemctl stop "$1"; } + +stage3_inert_pid_starttime() { + local pid=$1 + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + /usr/bin/awk 'NF >= 22 && $22 ~ /^[0-9]+$/ { print $22; found=1 } END { exit !found }' "/proc/$pid/stat" 2>/dev/null +} + +stage3_inert_pid_identity_matches() { + local pid=$1 expected=$2 current + [[ "$expected" =~ ^[0-9]+$ ]] || return 1 + current=$(stage3_inert_pid_starttime "$pid") || return 1 + [ "$current" = "$expected" ] +} + +stage3_inert_note_signal() { + [ "$inert_pending_signal" -ne 0 ] || inert_pending_signal=$1 +} + +stage3_inert_defer_signals() { + trap 'stage3_inert_note_signal 129' HUP + trap 'stage3_inert_note_signal 130' INT + trap 'stage3_inert_note_signal 143' TERM +} + +stage3_inert_arm_signals() { + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM +} + +stage3_inert_replay_pending_signal() { + local pending=$inert_pending_signal + inert_pending_signal=0 + [ "$pending" -eq 0 ] || return "$pending" +} + +stage3_inert_cleanup() { + local original_rc=$? cleanup_rc=0 show load show_rc invocation attempt stop_attempted=0 collected=0 not_found_streak=0 + trap - EXIT HUP INT TERM + if [ -n "$inert_client_pid" ] && /usr/bin/kill -0 "$inert_client_pid" 2>/dev/null; then + if stage3_inert_pid_identity_matches "$inert_client_pid" "$inert_client_starttime"; then + /usr/bin/kill -TERM "$inert_client_pid" 2>/dev/null || cleanup_rc=1 + wait "$inert_client_pid" 2>/dev/null || true + else + cleanup_rc=1 + wait "$inert_client_pid" 2>/dev/null || true + fi + fi + for attempt in {1..100}; do + if show=$(stage3_inert_show "$inert_unit" 2>/dev/null); then show_rc=0; else show_rc=$?; fi + if [ "$show_rc" -ne 0 ]; then not_found_streak=0; /usr/bin/sleep 0.05; continue; fi + load=$(stage3_show_value "$show" LoadState 2>/dev/null || true) + if [ "$load" = not-found ]; then + not_found_streak=$((not_found_streak + 1)) + if [ "$not_found_streak" -ge 3 ]; then collected=1; break; fi + /usr/bin/sleep 0.05 + continue + fi + not_found_streak=0 + if [ "$load" = loaded ] && [ "$inert_client_started" -eq 1 ]; then + invocation=$(stage3_show_value "$show" InvocationID 2>/dev/null || true) + if stage3_inert_unit_identity "$show" "$inert_unit" "${inert_invocation:-}" "$inert_activation" "$inert_token"; then + if [ "$inert_owned" -eq 0 ]; then inert_invocation=$invocation; inert_owned=1; fi + if [ "$stop_attempted" -eq 0 ]; then stage3_inert_stop "$inert_unit" >/dev/null 2>&1 || cleanup_rc=1; stop_attempted=1; fi + else + cleanup_rc=1 + break + fi + else + cleanup_rc=1 + break + fi + /usr/bin/sleep 0.05 + done + [ "$collected" -eq 1 ] || cleanup_rc=1 + stage3_cleanup_probe_exact "$inert_probe" "$inert_probe_uid" "$inert_probe_gid" "$inert_probe_mode" || cleanup_rc=1 + [ "$original_rc" -ne 0 ] || original_rc=$cleanup_rc + exit "$original_rc" +} + +stage3_inert_main() { + local compact_id show load run_rc=1 attempt show_rc capture_rc pending_rc + [ "$#" -eq 0 ] + [ "$(/usr/bin/id -u)" = 0 ] + [ "${PHASE2B_STAGE3_LOCK_HELD:-0}" = 1 ] + stage3_acquire_lock + stage3_read_marker + [ "${stage3_marker_fields[STATUS]}" = ACTIVE ] + inert_activation=${stage3_marker_fields[ACTIVATION_ID]} + compact_id=${inert_activation//-/} + inert_token=$(< /proc/sys/kernel/random/uuid) + inert_token=${inert_token//-/} + [[ "$inert_token" =~ ^[0-9a-f]{32}$ ]] + inert_unit="${inert_unit_prefix}${compact_id}.service" + trap stage3_inert_cleanup EXIT + stage3_inert_arm_signals + + [ "$(/usr/bin/systemctl show user@1200.service --property=ActiveState --value)" = inactive ] + [ "$(/usr/bin/systemctl show user@1200.service --property=SubState --value)" = dead ] + [ ! -e "$inert_probe" ] && [ ! -L "$inert_probe" ] + [ -x "$touch_wrapper" ] && [ -x "$rm_wrapper" ] + show=$(/usr/bin/systemctl show --no-pager "$inert_unit" --property=LoadState --property=ActiveState --property=FragmentPath) + stage3_inert_unit_unclaimed "$show" + + stage3_inert_defer_signals + /usr/bin/systemd-run --service-type=exec --wait --pipe --collect --unit="$inert_unit" --description="Phase 2B Stage 3 inert containment $inert_activation" \ + --uid=1200 --gid=1200 \ + --setenv=DOCKER_HOST=unix:///run/user/1200/docker.sock \ + --setenv="PHASE2B_STAGE3_INERT_TOKEN=$inert_token" \ + --property='UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy' \ + --property=RuntimeMaxSec=120s --property=TimeoutStopSec=15s --property=KillMode=control-group --property=UMask=0022 \ + --property=CPUQuota=600% --property=MemoryHigh=48G --property=MemoryMax=64G --property=MemorySwapMax=16G --property=TasksMax=4096 \ + --property=NetworkNamespacePath=/run/netns/le-app-codex \ + --property=ProtectSystem=strict \ + --property=PrivateTmp=yes \ + --property=ProtectHome=yes \ + --property=ProtectProc=invisible \ + --property=ProcSubset=all \ + --property=BindReadOnlyPaths=/etc/le-app-codex-runtime/resolv.conf:/etc/resolv.conf \ + --property=TemporaryFileSystem=/srv:ro \ + --property=BindPaths=/srv/le-app-codex:/srv/le-app-codex \ + --property=TemporaryFileSystem=/var:ro \ + --property=TemporaryFileSystem=/mnt:ro \ + --property=TemporaryFileSystem=/media:ro \ + --property=TemporaryFileSystem=/opt:ro \ + --property=InaccessiblePaths=/var/www \ + --property=InaccessiblePaths=/root \ + --property=InaccessiblePaths=/home \ + --property=InaccessiblePaths=/boot \ + --property=InaccessiblePaths=/efi \ + --property=InaccessiblePaths=/run/docker.sock \ + --property=InaccessiblePaths=/var/run/docker.sock \ + --property=InaccessiblePaths=/run/containerd/containerd.sock \ + --property=InaccessiblePaths=/run/podman \ + --property=InaccessiblePaths=/run/dbus/system_bus_socket \ + --property=InaccessiblePaths=/run/systemd/private \ + --property=InaccessiblePaths=/var/lib/docker \ + --property=InaccessiblePaths=/var/lib/containerd \ + --property=InaccessiblePaths=/var/lib/containers \ + --property=InaccessiblePaths=/etc/docker \ + --property=InaccessiblePaths=/etc/containers \ + --property=InaccessiblePaths=/etc/ssh \ + --property=InaccessiblePaths=/etc/NetworkManager/system-connections \ + --property=InaccessiblePaths=/etc/wireguard \ + --property=DevicePolicy=closed \ + --property='DeviceAllow=/dev/fuse rw' \ + --property='DeviceAllow=/dev/net/tun rw' \ + --property="BindReadOnlyPaths=$touch_wrapper:/usr/bin/touch" \ + --property="BindReadOnlyPaths=$rm_wrapper:/usr/bin/rm" \ + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh & + inert_client_pid=$! + inert_client_started=1 + if inert_client_starttime=$(stage3_inert_pid_starttime "$inert_client_pid"); then capture_rc=0; else capture_rc=$?; fi + stage3_inert_arm_signals + if stage3_inert_replay_pending_signal; then pending_rc=0; else pending_rc=$?; fi + [ "$pending_rc" -eq 0 ] || exit "$pending_rc" + [ "$capture_rc" -eq 0 ] + + for attempt in {1..100}; do + show=$(stage3_inert_show "$inert_unit" 2>/dev/null) || show= + load=$(stage3_show_value "$show" LoadState 2>/dev/null || true) + if [ "$load" = loaded ] && stage3_inert_unit_identity "$show" "$inert_unit" '' "$inert_activation" "$inert_token"; then + inert_invocation=$(stage3_show_value "$show" InvocationID) + inert_owned=1 + break + fi + /usr/bin/kill -0 "$inert_client_pid" 2>/dev/null || break + /usr/bin/sleep 0.05 + done + stage3_inert_defer_signals + if wait "$inert_client_pid"; then run_rc=0; else run_rc=$?; fi + stage3_inert_arm_signals + if stage3_inert_replay_pending_signal; then pending_rc=0; else pending_rc=$?; fi + [ "$pending_rc" -eq 0 ] || exit "$pending_rc" + inert_client_pid= + inert_client_starttime= + [ "$run_rc" -eq 0 ] + [ "$inert_owned" -eq 1 ] && [[ "$inert_invocation" =~ ^[0-9a-f]{32}$ ]] + stage3_cleanup_probe_exact "$inert_probe" "$inert_probe_uid" "$inert_probe_gid" "$inert_probe_mode" + for attempt in {1..100}; do + if load=$(/usr/bin/systemctl show "$inert_unit" --property=LoadState --value 2>/dev/null); then show_rc=0; else show_rc=$?; fi + [ "$show_rc" -eq 0 ] || break + [ "$load" = not-found ] && break + /usr/bin/sleep 0.05 + done + [ "$show_rc" -eq 0 ] && [ "$load" = not-found ] + [ "$(/usr/bin/systemctl show user@1200.service --property=ActiveState --value)" = inactive ] + trap - EXIT HUP INT TERM +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_inert_main "$@"; fi diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh new file mode 100644 index 0000000..9b0cbfa --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C + +failures=0 +stage3_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +migration_root=$(CDPATH= cd -- "$stage3_root/.." 2>/dev/null && pwd) +stage1_root=$migration_root/phase2b-contained-runtime +stage2_root=$migration_root/phase2b-stage2-verification +activate=$stage3_root/tests/50-activate-stage3.sh +rollback=$stage3_root/tests/51-rollback-stage3.sh +common=$stage3_root/tests/stage3-common.sh +runner=$stage3_root/tests/52-run-stage1-inert.sh +failure_suite=$stage3_root/tests/54-stage3-failure-path-tests.sh +installed_inert=$stage1_root/tests/10-inert-containment.sh + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } + +emit_array_lines() { + local file=$1 name=$2 + /usr/bin/awk -v name="$name" ' + $0 ~ "^[[:space:]]*" name "=\\($" { inside=1; next } + inside && $0 ~ "^[[:space:]]*\\)$" { exit } + inside { sub(/^[[:space:]]+/, ""); print } + ' "$file" +} + +verify_working_manifest_coverage() { + local line path file relative records=0 files=0 + declare -A listed=() + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^[0-9a-f]{64}\ \ (.+)$ ]]; then path=${BASH_REMATCH[1]}; else fail 'Stage 3 malformed manifest record'; continue; fi + if [[ "$path" = /* ]] || [[ "$path" = *'|'* ]] || [[ "$path" = ../* ]] || [[ "$path" = */../* ]] || [[ "$path" = */.. ]] || [ "$path" = . ] || [ "$path" = .. ] || [ "$path" = MANIFEST.sha256 ] || [ -n "${listed[$path]:-}" ]; then fail "Stage 3 unsafe or duplicate manifest record $path"; else listed["$path"]=1; records=$((records + 1)); fi + done < "$stage3_root/MANIFEST.sha256" + while IFS= read -r -d '' file; do + relative=${file#"$stage3_root"/} + [ "$relative" = MANIFEST.sha256 ] && continue + files=$((files + 1)) + [ -n "${listed[$relative]:-}" ] || fail "Stage 3 manifest omits $relative" + done < <(/usr/bin/find "$stage3_root" -type f -print0) + if /usr/bin/find "$stage3_root" -type l -print -quit | /usr/bin/grep -q .; then fail 'Stage 3 package contains a symlink'; fi + if [ "$records" -eq "$files" ] && [ "$records" -gt 0 ]; then pass "Stage 3 manifest covers $records working files"; else fail "Stage 3 manifest coverage records=$records files=$files"; fi +} + +syntax_failures=0 +for file in "$activate" "$rollback" "$common" "$runner" "$stage3_root/tests/53-stage3-static-tests.sh" "$failure_suite"; do /bin/bash -n "$file" || syntax_failures=$((syntax_failures + 1)); done +for file in "$stage3_root"/tests/inert-bin/*; do /bin/sh -n "$file" || syntax_failures=$((syntax_failures + 1)); done +[ "$syntax_failures" -eq 0 ] && pass 'Stage 3 shell syntax' || fail 'Stage 3 shell syntax' +[ -x "$stage3_root/tests/inert-bin/touch" ] && [ -x "$stage3_root/tests/inert-bin/rm" ] && pass 'inert probe wrappers executable' || fail 'inert probe wrapper modes' +(cd "$stage1_root" && /usr/bin/sha256sum -c MANIFEST.sha256 >/dev/null) && pass 'unchanged Stage 1 manifest' || fail 'Stage 1 manifest' +(cd "$stage2_root" && /usr/bin/sha256sum -c MANIFEST.sha256 >/dev/null) && pass 'unchanged Stage 2 manifest' || fail 'Stage 2 manifest' +(cd "$stage3_root" && /usr/bin/sha256sum -c MANIFEST.sha256 >/dev/null) && pass 'Stage 3 manifest' || fail 'Stage 3 manifest' +verify_working_manifest_coverage + +repo_root=$(/usr/bin/git -C "$stage3_root" rev-parse --show-toplevel 2>/dev/null) +stage1_relative=${stage1_root#"$repo_root"/} +stage2_relative=${stage2_root#"$repo_root"/} +if [ -z "$(/usr/bin/git -C "$repo_root" status --porcelain --untracked-files=all -- "$stage1_relative" "$stage2_relative")" ] && /usr/bin/git -C "$repo_root" diff --quiet HEAD -- "$stage1_relative" "$stage2_relative"; then pass 'Stage 1 and Stage 2 trees byte-for-byte unchanged'; else fail 'Stage 1 or Stage 2 tree changed'; fi +[ "$(/usr/bin/sha256sum "$stage1_root/MANIFEST.sha256" | /usr/bin/cut -d' ' -f1)" = 5aaa91b1e6846eda033961dcee392b9657476ad6fd149420979e9309b484445f ] && [ "$(/usr/bin/sha256sum "$stage2_root/MANIFEST.sha256" | /usr/bin/cut -d' ' -f1)" = 4ce1de182dd6fda902d910c1d6f3f169dce55266e61f9abf2e57cd119fad600d ] && pass 'Stage 1/2 provenance digests pinned' || fail 'Stage 1/2 provenance digest drift' + +stage2_operator=$stage2_root/tests/40-verify-stage2.sh +if /usr/bin/diff -u <(emit_array_lines "$stage2_operator" sources) <(emit_array_lines "$common" sources) >/dev/null && /usr/bin/diff -u <(emit_array_lines "$stage2_operator" destinations) <(emit_array_lines "$common" destinations) >/dev/null; then pass 'Stage 3 16-file map exactly matches Stage 2'; else fail 'Stage 3 16-file map differs from Stage 2'; fi + +start_count=$(/usr/bin/grep -Fc '/usr/bin/systemctl start le-app-codex-netns.service' "$activate") +stop_count=$(/usr/bin/grep -Fc '/usr/bin/systemctl stop le-app-codex-netns.service' "$rollback") +nft_check_count=$(/usr/bin/grep -Fc '/usr/bin/nft --check -f "$installed_policy"' "$activate") +[ "$start_count" -eq 1 ] && [ "$stop_count" -eq 1 ] && [ "$nft_check_count" -eq 1 ] && pass 'exact activation/rollback/nft-check command counts' || fail "command counts start=$start_count stop=$stop_count nft-check=$nft_check_count" +[ "$(/usr/bin/grep -hFo '/usr/bin/systemctl start ' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 1 ] && [ "$(/usr/bin/grep -hFo '/usr/bin/systemctl stop ' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_stop() { /usr/bin/systemctl stop "$1"; }' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_stop "$inert_unit"' "$runner")" -eq 1 ] && pass 'only namespace activation plus token-verified transient cleanup can start/stop units' || fail 'unexpected systemctl start/stop surface' +systemctl_verbs=$(/usr/bin/grep -hEo '/usr/bin/systemctl[[:space:]]+[[:alnum:]-]+' "$activate" "$rollback" "$common" "$runner" | /usr/bin/awk '{ print $2 }' | /usr/bin/sort -u) +[ "$systemctl_verbs" = $'cat\nis-enabled\nlist-units\nshow\nstart\nstop' ] && pass 'systemctl verb surface is a closed read/start/verified-stop allowlist' || fail "unexpected systemctl verb surface: $systemctl_verbs" +if /usr/bin/grep -Eq '^[[:space:]]*/usr/bin/systemctl[[:space:]]+(enable|disable|restart|reenable|preset|mask|unmask)|^[[:space:]]*/usr/bin/systemctl[[:space:]]+start[[:space:]]+user@1200|^[[:space:]]*/usr/bin/loginctl|^[[:space:]]*/usr/bin/nft[[:space:]]+-f|^[[:space:]]*/usr/local/libexec/le-app-codex-netns[[:space:]]+down|^[[:space:]]*/usr/bin/(docker|codex)[[:space:]]+(run|start|login|auth)|git[[:space:]]+clone|^[[:space:]]*/usr/bin/(cp|scp|rsync)[[:space:]]|/srv/[^[:space:]]*secret' "$activate" "$rollback" "$common" "$runner"; then fail 'prohibited activation, service, copy, or secret command literal'; else pass 'no prohibited activation, service, copy, or secret command literal'; fi +if /usr/bin/grep -Eq '/usr/bin/nft[[:space:]]+--numeric|/usr/bin/nft[[:space:]]+-f|/usr/bin/systemctl[[:space:]]+start[[:space:]]+(user@1200|docker|codex)|/usr/bin/systemctl[[:space:]]+enable' "$activate" "$rollback" "$common" "$runner"; then fail 'numeric/load or prohibited service command literal'; else pass 'no numeric nft output, nft load, or prohibited service command literal'; fi +[ "$(/usr/bin/grep -hFo '/usr/bin/nft delete ' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 2 ] && [ "$(/usr/bin/grep -hFo '/usr/bin/ip link delete ' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 1 ] && [ "$(/usr/bin/grep -hFo '/usr/bin/ip netns delete ' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 1 ] && [ "$(/usr/bin/grep -hE '/usr/bin/ip.*[[:space:]]delete[[:space:]]' "$activate" "$rollback" "$common" "$runner" | /usr/bin/wc -l)" -eq 2 ] && ! /usr/bin/grep -Eq '/usr/bin/nft[[:space:]]+([^#;|]*[[:space:]])?(add|insert|replace|flush|reset|create|destroy|rename|import|monitor)[[:space:]]' "$activate" "$rollback" "$common" "$runner" && ! /usr/bin/grep -Eq '/usr/bin/ip[[:space:]]+([^#;|]*[[:space:]])?(add|set|replace|flush)[[:space:]]' "$activate" "$rollback" "$common" "$runner" && pass 'nft/ip mutations are closed to two exact table deletes, one veth delete, and one namespace delete' || fail 'unexpected nft/ip mutation surface' +[ "$(/usr/bin/grep -Fc 'stage3_nft_json_inet_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_nft_json_nat_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_inet_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nat_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_capture_exact_project_policy_snapshot' "$activate")" -eq 4 ] && ! /usr/bin/grep -q 'stage3_nft_text_chain' "$activate" "$rollback" "$common" && pass 'activation and rollback share exact canonical JSON policy validation' || fail 'nft JSON validation surface' +[ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table inet le_app_codex' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table ip le_app_codex_nat' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip link delete lecodex-host' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip netns delete le-app-codex' "$rollback")" -eq 1 ] && pass 'rollback direct cleanup limited to exact project names' || fail 'rollback direct cleanup literals' +if [ "$(/usr/bin/grep -Fc '/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/docker' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ' "$common")" -eq 4 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network ls --no-trunc -q' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ps --no-trunc -aq' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '-u DOCKER_API_VERSION -u DOCKER_CERT_PATH -u DOCKER_CONTEXT -u DOCKER_HOST -u DOCKER_TLS -u DOCKER_TLS_VERIFY' "$common")" -eq 1 ] && /usr/bin/jq -e 'type == "object" and length == 0' "$stage3_root/tests/docker-config/config.json" >/dev/null; then pass 'all rootful Docker reads use one context-scrubbed explicit-socket read allowlist and empty config'; else fail 'rootful Docker helper/socket/config surface'; fi +[ "$(/usr/bin/grep -Fc '/usr/bin/systemd-run --service-type=exec --wait --pipe --collect --unit="$inert_unit"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/srv/le-app-codex/phase2b-tests/10-inert-containment.sh' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '--property=RuntimeMaxSec=120s' "$runner")" -eq 1 ] && pass 'finite collected wrapper executes existing installed inert test' || fail 'inert test wrapper command surface' +[ "$(/usr/bin/grep -Fc -- "--property='UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy'" "$runner")" -eq 1 ] && ! /usr/bin/grep -Eq -- '--setenv=(ALL_PROXY|HTTPS_PROXY|HTTP_PROXY|NO_PROXY|all_proxy|https_proxy|http_proxy|no_proxy)' "$runner" && pass 'transient inert test removes every proxy variable' || fail 'transient inert proxy environment' +[ "$(/usr/bin/grep -Fc -- '--setenv="PHASE2B_STAGE3_INERT_TOKEN=$inert_token"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$inert_owned" -eq 1 ] && [[ "$inert_invocation" =~ ^[0-9a-f]{32}$ ]]' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_unit_identity "$show" "$inert_unit" "${inert_invocation:-}" "$inert_activation" "$inert_token"' "$runner")" -eq 1 ] && pass 'transient success and cleanup require token-bound captured ownership' || fail 'transient ownership/token gate' +if /usr/bin/awk ' + /^ stage3_inert_defer_signals$/ && state == 0 { state=1; next } + state == 1 && /systemd-run --service-type=exec/ { state=2; next } + state == 2 && /^ inert_client_pid=\$!$/ { state=3; next } + state == 3 && /^ inert_client_started=1$/ { state=4; next } + state == 4 && /inert_client_starttime=.*stage3_inert_pid_starttime/ { state=5; next } + state == 5 && /^ stage3_inert_arm_signals$/ { state=6; next } + state == 6 && /stage3_inert_replay_pending_signal/ { state=7 } + END { exit state != 7 } +' "$runner" && [ "$(/usr/bin/grep -Fc 'if stage3_inert_pid_identity_matches "$inert_client_pid" "$inert_client_starttime"; then' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$not_found_streak" -ge 3 ]' "$runner")" -eq 1 ]; then pass 'inert launch defers signals through token/PID ownership and cleanup requires PID identity plus stable collection'; else fail 'inert launch/cleanup race hardening surface'; fi +[ "$(/usr/bin/grep -Ec '/usr/bin/ip netns exec le-app-codex .* /usr/bin/curl ' "$activate")" -eq 4 ] && ! /usr/bin/grep -E '/usr/bin/ip netns exec le-app-codex .* /usr/bin/curl ' "$activate" | /usr/bin/grep -Fv '"${proxy_free_env[@]}" /usr/bin/curl --noproxy' >/dev/null && [ "$(/usr/bin/grep -Fc -- '-u ALL_PROXY -u HTTPS_PROXY -u HTTP_PROXY -u NO_PROXY -u all_proxy -u https_proxy -u http_proxy -u no_proxy /usr/bin/curl --noproxy' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'expect_denied curl' "$installed_inert")" -ge 1 ] && pass 'all namespace/public-IP curl probes have proxy removal' || fail 'curl proxy-removal coverage' +[ "$(/usr/bin/grep -Fc "printf 'ACTIVATION_ID|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'MACHINE_ID_SHA256|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'APPROVED_DNS4|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'APPROVED_ARTIFACT_MANIFEST_SHA256|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'STAGE1_SOURCE_COMMIT|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'STAGE2_VERIFICATION_COMMIT|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'TIMESTAMP|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'HOSTNAME|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'NFT_INET_SHA256|%s" "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "printf 'NFT_NAT_SHA256|%s" "$common")" -eq 1 ] && pass 'required marker provenance and per-table fields emitted once' || fail 'required marker provenance fields' +[ "$(/usr/bin/grep -Fc 'stage3_atomic_publish_new "$marker" "$rollback_record"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/rm -- "$marker"' "$rollback")" -eq 0 ] && pass 'rollback atomically retains activation provenance' || fail 'rollback tombstone transition' +[ "$(/usr/bin/grep -Fc '[[ "$line" =~ ^([^|]+)\|([^|]+)$ ]]' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$parsed_size" -eq "$record_size" ]' "$common")" -eq 1 ] && ! /usr/bin/grep -Fq "IFS='|' read -r key value extra" "$common" && pass 'record parser requires canonical one-delimiter newline-complete bytes' || fail 'record parser exact-byte surface' +[ "$(/usr/bin/grep -n '/usr/bin/sync -f "$temporary"' "$rollback" | /usr/bin/cut -d: -f1)" -lt "$(/usr/bin/grep -n '/usr/bin/mv -T -- "$temporary" "$rollback_record"' "$rollback" | /usr/bin/cut -d: -f1)" ] && [ "$(/usr/bin/grep -Fc 'stage3_validate_safe_temporary "$temporary" "$rollback_record" "$expected_uid" "$expected_gid" "$require_provenance"' "$rollback")" -eq 2 ] && pass 'recovered ACTIVE temporary is fsynced and revalidated before tombstone replacement' || fail 'ACTIVE temporary durability/revalidation order' +[ "$(/usr/bin/grep -n '/usr/bin/sync -f "$marker_temporary"' "$rollback" | /usr/bin/cut -d: -f1)" -lt "$(/usr/bin/grep -n 'stage3_atomic_publish_new "$marker_temporary" "$rollback_record"' "$rollback" | /usr/bin/cut -d: -f1)" ] && pass 'temp-only complete record is fsynced before retained-authority publication' || fail 'temp-only authority durability order' +[ "$(/usr/bin/grep -Fc 'stage3_validate_safe_temporary "$marker_temporary" "$authority_path"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_record_common_sha256' "$rollback")" -ge 2 ] && pass 'temporary publication state is provenance-compared before mutation' || fail 'temporary record validation surface' +[ "$(/usr/bin/grep -Fc 'PHASE2B_STAGE3_LOCK_ID' "$activate")" -ge 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_validate_inherited_lock 9 /etc/le-app-codex-runtime' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_lock_descriptor_matches 9' "$common")" -eq 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_acquire_lock' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_validate_inherited_lock 9 "$root"' "$failure_suite")" -eq 2 ] && pass 'activation, automatic rollback, inert child, and tests validate one inherited lock/token' || fail 'inherited lock validation surface' +[ "$(/usr/bin/grep -Fc 'stage3_verify_loaded_units inactive' "$activate")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'systemctl cat --no-pager user@1200.service' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_write_marker PREPARED' "$activate")" -eq 1 ] && [ "$(/usr/bin/grep -n 'stage3_verify_loaded_units inactive' "$activate" | /usr/bin/cut -d: -f1)" -lt "$(/usr/bin/grep -n 'stage3_write_marker PREPARED' "$activate" | /usr/bin/cut -d: -f1)" ] && pass 'loaded-unit fragment/drop-in/property gate is final before marker/start' || fail 'loaded-unit validation placement or surface' +[ "$(/usr/bin/grep -Fc 'stage3_capture_active_topology_identity' "$activate")" -eq 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_read_exact_project_policy_snapshot' "$common")" -eq 3 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nft_canonical_from_tables "$inet_canonical" "$nat_canonical"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nft_sha256=$stage3_snapshot_project_sha256' "$activate")" -eq 1 ] && /usr/bin/grep -Fq '[ "$host_ifindex" = "$peer_iflink" ] && [ "$host_iflink" = "$peer_ifindex" ]' "$common" && pass 'active topology is reciprocal and project provenance comes from two matching exact table snapshots' || fail 'active topology/stable nft snapshot surface' +[ "$(/usr/bin/grep -Fc 'type == "array" and all(.[];' "$common")" -ge 5 ] && [ "$(/usr/bin/grep -Fc 'error("invalid link inventory")' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'error("invalid nft table inventory")' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '.ifname != "lecodex-host" and .ifname != "lecodex-ns"' "$common")" -eq 1 ] && pass 'typed inventories reject wrong shapes and both project peers are baseline-excluded' || fail 'typed inventory/baseline exclusion surface' +[ "$(/usr/bin/grep -Fc 'stage3_resource_presence_shape_valid' "$rollback")" -eq 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_resource_presence_shape_valid' "$failure_suite")" -ge 12 ] && [ "$(/usr/bin/grep -Fc 'stage3_prepared_live_invocation_valid' "$rollback")" -eq 2 ] && [ "$(/usr/bin/grep -Fc 'case_prepared_tombstone_retry_without_live_invocation' "$failure_suite")" -eq 2 ] && pass 'ordered partial survivors and PREPARED tombstone retry attribution are enforced and tested' || fail 'partial survivor/retry surface' +[ "$(/usr/bin/grep -Fc 'stage3_materialize_docker_ipv4_gateways' "$activate")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '"$raw_count" -eq 39' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '"$unique_count" -eq 39' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -n 'stage3_materialize_docker_ipv4_gateways "$current_docker"' "$activate" | /usr/bin/cut -d: -f1)" -lt "$(/usr/bin/grep -n "stage3_expect_blocked_counter 'non-Quad9 UDP DNS'" "$activate" | /usr/bin/cut -d: -f1)" ] && pass 'all 39 unique Docker gateways are required before any denial probe' || fail 'Docker gateway materialization surface/order' +[ "$(/usr/bin/grep -Fc '/usr/bin/ps -eo comm=' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '$1 == "codex"' "$common")" -eq 1 ] && pass 'global Codex-name inventory complements complete UID-1200 process absence' || fail 'Codex process inventory surface' +[ "$(/usr/bin/grep -Fc "stage3_compare_recorded_baseline 'pre-rollback'" "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "stage3_compare_recorded_baseline 'post-rollback'" "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "stage3_compare_recorded_baseline 'changed-boot pre-rollback'" "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "stage3_compare_recorded_baseline 'changed-boot post-rollback'" "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_compare_rollback_baseline' "$rollback")" -ge 2 ] && pass 'recorded and immediate baselines gate same-boot and changed-boot rollback/recovery' || fail 'rollback baseline comparison surface' +[ "$(/usr/bin/grep -Fc 'stage3_capture_resource_inventory || return 1' "$rollback")" -ge 4 ] && [ "$(/usr/bin/grep -Fc 'stage3_table_identity "$family" "$name" || return 1' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_veth_identity || return 1' "$rollback")" -ge 2 ] && [ "$(/usr/bin/grep -Fc 'stage3_namespace_identity || return 1' "$rollback")" -ge 2 ] && pass 'every direct cleanup path performs fresh inventory and identity checks' || fail 'direct cleanup revalidation surface' +[ "$(/usr/bin/grep -Fc 'if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_activation_main "$@"; fi' "$activate")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_rollback_main "$@"; fi' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then stage3_inert_main "$@"; fi' "$runner")" -eq 1 ] && pass 'operational entry points are source-guarded' || fail 'operational source guard' +[ "$(/usr/bin/grep -Fc '/srv/le-app-codex/tmp/phase2b-write-test' "$stage3_root/tests/inert-bin/touch")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/srv/le-app-codex/tmp/phase2b-write-test' "$stage3_root/tests/inert-bin/rm")" -eq 1 ] && pass 'inert probe wrappers accept only legacy exact path' || fail 'inert probe wrapper path gate' +[ "$(/usr/bin/grep -Fc 'sudo /bin/bash docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/50-activate-stage3.sh' "$stage3_root/README.md")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'sudo /bin/bash docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/51-rollback-stage3.sh' "$stage3_root/README.md")" -eq 1 ] && /usr/bin/grep -Fq 'Only after this package has been reviewed, committed, and checked out as a clean worktree on TITANSERVER' "$stage3_root/README.md" && pass 'future activation and rollback invocations are exact and review-gated' || fail 'future operator invocation documentation' + +failures_before=$failures +source "$common" +committed_networks=$(stage3_committed_docker_network_canonical) +printf '%s' "$committed_networks" | /usr/bin/jq -e 'length == 41' >/dev/null && pass 'committed Docker canonicalizer yields 41 networks' || fail 'committed Docker canonicalizer' +[ "$failures" -eq "$failures_before" ] || fail 'common library source has side effects' + +/bin/bash "$stage1_root/tests/32-stage1-state-tests.sh" >/dev/null && pass 'existing non-mutating Stage 1 state tests' || fail 'Stage 1 state tests' +/bin/bash "$stage2_root/tests/41-stage2-static-tests.sh" >/dev/null && pass 'existing non-mutating Stage 2 static tests' || fail 'Stage 2 static tests' +/bin/bash "$failure_suite" >/dev/null && pass 'Stage 3 non-mutating failure-path suite' || fail 'Stage 3 failure-path suite' + +printf 'Phase 2B Stage 3 static tests failures=%s\n' "$failures" +[ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh new file mode 100644 index 0000000..75b3ff1 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh @@ -0,0 +1,632 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C + +failures=0 +stage3_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) +source "$stage3_root/tests/stage3-common.sh" +source "$stage3_root/tests/51-rollback-stage3.sh" + +pass() { printf 'PASS: %s\n' "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); } +expect_success() { local label=$1; shift; if ( "$@" ); then pass "$label"; else fail "$label"; fi; } +expect_failure() { local label=$1; shift; if ( "$@" ); then fail "$label"; else pass "$label"; fi; } + +fixture=$(/usr/bin/mktemp -d /tmp/le-app-codex-stage3-fixture.XXXXXX) +case "$fixture" in /tmp/le-app-codex-stage3-fixture.*) ;; *) printf 'unsafe fixture root\n' >&2; exit 1 ;; esac +trap '/usr/bin/rm -rf -- "$fixture"' EXIT HUP INT TERM +/usr/bin/chmod 0755 "$fixture" + +write_prepared_record() { + local target=$1 + stage3_activation_id=11111111-2222-4333-8444-555555555555 + stage3_machine_id_sha256=$(printf 'a%.0s' {1..64}) + stage3_head=$(printf 'b%.0s' {1..40}) + stage3_timestamp=2026-07-13T12:00:00Z + stage3_hostname=fixture-host + stage3_boot_id=aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee + baseline_public_ipv4=$expected_public_ipv4 + baseline_nft_sha256=$(printf 'c%.0s' {1..64}) + baseline_docker_network_sha256=$(printf 'd%.0s' {1..64}) + baseline_host_network_sha256=$(printf 'e%.0s' {1..64}) + baseline_docker_runtime_sha256=$(printf 'f%.0s' {1..64}) + stage3_render_record PREPARED > "$target" + /usr/bin/chmod 0600 "$target" +} + +write_active_record() { + local target=$1 + write_prepared_record "$target" + stage3_service_invocation_id=$(printf '1%.0s' {1..32}) + stage3_namespace_dev_inode=ab:12345 + stage3_host_veth_ifindex=101 + stage3_host_veth_iflink=102 + stage3_host_veth_mac=02:11:22:33:44:55 + stage3_nft_inet_handle=201 + stage3_nft_nat_handle=202 + stage3_nft_inet_sha256=$(printf '6%.0s' {1..64}) + stage3_nft_nat_sha256=$(printf '7%.0s' {1..64}) + stage3_project_nft_sha256=$(printf '8%.0s' {1..64}) + stage3_render_record ACTIVE > "$target" + /usr/bin/chmod 0600 "$target" +} + +emit_fixture_inert_show() { + local unit=$1 activation=$2 token=$3 invocation=$4 + printf 'LoadState=loaded\n' + printf 'ActiveState=active\n' + printf 'Transient=yes\n' + printf 'FragmentPath=/run/systemd/transient/%s\n' "$unit" + printf 'User=1200\nGroup=1200\n' + printf 'NetworkNamespacePath=/run/netns/le-app-codex\n' + printf 'Description=Phase 2B Stage 3 inert containment %s\n' "$activation" + printf 'Environment=DOCKER_HOST=unix:///run/user/1200/docker.sock PHASE2B_STAGE3_INERT_TOKEN=%s\n' "$token" + printf 'UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy\n' + printf 'ExecStart={ path=/srv/le-app-codex/phase2b-tests/10-inert-containment.sh ; argv[]=/srv/le-app-codex/phase2b-tests/10-inert-containment.sh ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\n' + printf 'InvocationID=%s\n' "$invocation" +} + +case_marker_publication() { + local root=$fixture/marker source target before_hash after_hash + source=$root/new; target=$root/marker + /usr/bin/mkdir "$root" + write_prepared_record "$source" + [ ! -e "$target" ] || return 1 + before_hash=$(/usr/bin/sha256sum "$source" | /usr/bin/cut -d' ' -f1) + stage3_atomic_publish_new "$source" "$target" || return 1 + [ ! -e "$source" ] && [ -f "$target" ] || return 1 + after_hash=$(/usr/bin/sha256sum "$target" | /usr/bin/cut -d' ' -f1) + [ "$before_hash" = "$after_hash" ] && stage3_parse_record_file "$target" 0 +} + +case_marker_no_clobber() { + local root=$fixture/no-clobber source target hash + source=$root/new; target=$root/marker + /usr/bin/mkdir "$root" + write_prepared_record "$source" + printf 'malformed\n' > "$target" + hash=$(/usr/bin/sha256sum "$target" | /usr/bin/cut -d' ' -f1) + ! stage3_atomic_publish_new "$source" "$target" && [ -f "$source" ] && [ "$hash" = "$(/usr/bin/sha256sum "$target" | /usr/bin/cut -d' ' -f1)" ] +} + +case_complete_temporary_becomes_durable_authority() { + local root=$fixture/temp-only uid gid hash + root=$fixture/temp-only; /usr/bin/mkdir "$root" + uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + marker=$root/marker; rollback_record=$root/tomb; marker_temporary=$root/new + write_prepared_record "$marker_temporary" + hash=$(/usr/bin/sha256sum "$marker_temporary" | /usr/bin/cut -d' ' -f1) + stage3_read_marker() { return 1; } + stage3_read_rollback_record() { stage3_parse_record_file "$rollback_record" 0; } + stage3_resources_absent() { return 0; } + stage3_select_authoritative_record "$uid" "$gid" 0 || return 1 + [ "$record_location" = tombstone ] && [ ! -e "$marker_temporary" ] && [ "$hash" = "$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_read_rollback_record && [ "${stage3_marker_fields[STATUS]}" = PREPARED ] +} + +case_interrupted_marker_temporary() { + local root=$fixture/interrupted-temp authority temporary uid gid + root=$fixture/interrupted-temp; authority=$root/marker; temporary=$root/new + /usr/bin/mkdir "$root"; uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + write_prepared_record "$authority" + marker=$authority + stage3_read_marker() { stage3_parse_record_file "$marker" 0; } + stage3_read_rollback_record() { return 1; } + stage3_parse_record_file "$authority" 0 || return 1 + : > "$temporary"; /usr/bin/chmod 0600 "$temporary" + stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 || return 1 + write_active_record "$temporary" + stage3_parse_record_file "$authority" 0 || return 1 + stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 || return 1 + /usr/bin/sed -i 's/TIMESTAMP|2026-07-13T12:00:00Z/TIMESTAMP|2026-07-13T12:00:01Z/' "$temporary" + stage3_parse_record_file "$authority" 0 || return 1 + ! stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 || return 1 + printf 'partial-nonzero' > "$temporary"; /usr/bin/chmod 0600 "$temporary" + stage3_parse_record_file "$authority" 0 || return 1 + ! stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 +} + +case_conflicting_active_temporary() { + local root=$fixture/conflicting-active authority temporary uid gid + root=$fixture/conflicting-active; authority=$root/tomb; temporary=$root/new + /usr/bin/mkdir "$root"; uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + write_active_record "$authority"; /usr/bin/cp "$authority" "$temporary"; /usr/bin/chmod 0600 "$temporary" + rollback_record=$authority + stage3_read_rollback_record() { stage3_parse_record_file "$rollback_record" 0; } + stage3_read_marker() { return 1; } + stage3_parse_record_file "$authority" 0 || return 1 + stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 || return 1 + /usr/bin/sed -i 's/NFT_INET_HANDLE|201/NFT_INET_HANDLE|999/' "$temporary" + stage3_parse_record_file "$authority" 0 || return 1 + ! stage3_validate_safe_temporary "$temporary" "$authority" "$uid" "$gid" 0 +} + +case_tombstone_publication() { + local root=$fixture/tomb marker_path tomb_path hash + marker_path=$root/marker; tomb_path=$root/tomb + /usr/bin/mkdir "$root" + write_prepared_record "$marker_path" + hash=$(/usr/bin/sha256sum "$marker_path" | /usr/bin/cut -d' ' -f1) + [ -f "$marker_path" ] && [ ! -e "$tomb_path" ] || return 1 + stage3_atomic_publish_new "$marker_path" "$tomb_path" || return 1 + [ ! -e "$marker_path" ] && [ -f "$tomb_path" ] && [ "$hash" = "$(/usr/bin/sha256sum "$tomb_path" | /usr/bin/cut -d' ' -f1)" ] || return 1 + ! stage3_atomic_publish_new "$marker_path" "$tomb_path" +} + +case_durable_active_temporary_upgrade() { + local root=$fixture/durable-upgrade uid gid active_hash + root=$fixture/durable-upgrade; /usr/bin/mkdir "$root" + marker=$root/marker; rollback_record=$root/tomb; marker_temporary=$root/new + uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + write_prepared_record "$rollback_record" + write_active_record "$marker_temporary" + active_hash=$(/usr/bin/sha256sum "$marker_temporary" | /usr/bin/cut -d' ' -f1) + stage3_read_marker() { return 1; } + stage3_read_rollback_record() { stage3_parse_record_file "$rollback_record" 0; } + stage3_read_rollback_record || return 1 + stage3_cleanup_safe_temporary "$marker_temporary" "$uid" "$gid" 0 || return 1 + [ ! -e "$marker_temporary" ] && [ "$active_hash" = "$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_read_rollback_record && [ "${stage3_marker_fields[STATUS]}" = ACTIVE ] +} + +case_partial_activation_and_rollback_retry() { + local root=$fixture/partial-activation retained_hash + root=$fixture/partial-activation; /usr/bin/mkdir "$root" + marker=$root/marker; rollback_record=$root/tomb; marker_temporary=$root/new + write_prepared_record "$marker" + stage3_read_marker() { stage3_parse_record_file "$marker" 0; } + stage3_read_rollback_record() { stage3_parse_record_file "$rollback_record" 0; } + stage3_select_authoritative_record || return 1 + [ "$record_location" = marker ] || return 1 + stage3_resource_presence_shape_valid 0 0 0 0 0 0 || return 1 + stage3_resource_presence_shape_valid 1 0 0 0 0 0 || return 1 + stage3_resource_presence_shape_valid 1 1 1 0 0 0 || return 1 + stage3_resource_presence_shape_valid 1 1 0 1 0 0 || return 1 + stage3_resource_presence_shape_valid 1 1 0 1 1 1 || return 1 + ! stage3_resource_presence_shape_valid 0 1 1 0 0 0 || return 1 + ! stage3_resource_presence_shape_valid 1 1 1 1 0 0 || return 1 + ! stage3_resource_presence_shape_valid 1 0 0 0 1 0 || return 1 + declare -gA stage3_marker_fields=([STATUS]=PREPARED) + test_table_present=1 test_shape_ok=1 + stage3_inventory_has_table() { [ "$test_table_present" -eq 1 ]; } + stage3_prepared_table_shape() { [ "$test_shape_ok" -eq 1 ]; } + stage3_table_identity inet le_app_codex || return 1 + test_shape_ok=0 + ! stage3_table_identity inet le_app_codex || return 1 + test_table_present=0 + stage3_table_identity inet le_app_codex || return 1 + stage3_atomic_publish_new "$marker" "$rollback_record" || return 1 + retained_hash=$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1) + stage3_resource_presence_shape_valid 1 0 0 0 0 0 || return 1 + record_location= + stage3_select_authoritative_record || return 1 + [ "$record_location" = tombstone ] && [ ! -e "$marker" ] && [ "$retained_hash" = "$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_resource_presence_shape_valid 0 0 0 0 0 0 +} + +case_prepared_tombstone_retry_without_live_invocation() { + declare -gA stage3_marker_fields=([STATUS]=PREPARED) + record_location=marker + ! stage3_prepared_live_invocation_valid 1 '' || return 1 + stage3_prepared_live_invocation_valid 1 "$(printf 'a%.0s' {1..32})" || return 1 + record_location=tombstone + stage3_prepared_live_invocation_valid 1 '' || return 1 + stage3_prepared_live_invocation_valid 0 '' +} + +case_partial_rollback_retry_and_nft_drift() { + local exact_hash root=$fixture/partial-rollback retained_hash + root=$fixture/partial-rollback; /usr/bin/mkdir "$root" + marker=$root/marker; rollback_record=$root/tomb; marker_temporary=$root/new + write_active_record "$marker" + stage3_read_marker() { stage3_parse_record_file "$marker" 0; } + stage3_read_rollback_record() { stage3_parse_record_file "$rollback_record" 0; } + stage3_select_authoritative_record || return 1 + stage3_atomic_publish_new "$marker" "$rollback_record" || return 1 + retained_hash=$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1) + exact_hash=$(printf 'inet' | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + declare -gA stage3_marker_fields=([STATUS]=ACTIVE [NFT_INET_HANDLE]=42 [NFT_INET_SHA256]="$exact_hash") + test_table_present=0 + stage3_inventory_has_table() { [ "$test_table_present" -eq 1 ]; } + stage3_inventory_table_handle() { printf '%s' "$test_handle"; } + stage3_nft_table_canonical() { printf '%s' "$test_table_body"; } + stage3_table_identity inet le_app_codex || return 1 + test_table_present=1 test_handle=42 test_table_body=inet + stage3_table_identity inet le_app_codex || return 1 + test_handle=43 + ! stage3_table_identity inet le_app_codex || return 1 + test_handle=42 test_table_body=drift + ! stage3_table_identity inet le_app_codex || return 1 + stage3_resource_presence_shape_valid 1 1 0 1 0 1 || return 1 + stage3_resource_presence_shape_valid 1 1 0 1 0 0 || return 1 + record_location= + stage3_select_authoritative_record || return 1 + [ "$record_location" = tombstone ] && [ "$retained_hash" = "$(/usr/bin/sha256sum "$rollback_record" | /usr/bin/cut -d' ' -f1)" ] || return 1 + stage3_resource_presence_shape_valid 1 0 0 0 0 0 || return 1 + stage3_resource_presence_shape_valid 0 0 0 0 0 0 +} + +case_inventory_failures() { + stage3_inventory_namespaces=stale stage3_inventory_links=stale stage3_inventory_tables=stale + stage3_query_namespaces() { return 1; } + ! stage3_capture_resource_inventory && [ -z "$stage3_inventory_namespaces$stage3_inventory_links$stage3_inventory_tables" ] || return 1 + stage3_query_namespaces() { printf '../escape\n'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_namespaces() { printf ''; } + stage3_query_links_json() { return 1; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_links_json() { printf 'not-json'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_links_json() { printf '{}'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_links_json() { printf '[null]'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_links_json() { printf '[{}]'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_links_json() { printf '[{"ifname":"lo","ifindex":1}]'; } + stage3_query_tables_json() { return 1; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_tables_json() { printf 'not-json'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_tables_json() { printf '{"nftables":{}}'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_tables_json() { printf '{"nftables":[{"error":"query failed"}]}'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_tables_json() { printf '{"nftables":[{"table":{"family":"inet","name":"x"}}]}'; } + ! stage3_capture_resource_inventory 2>/dev/null || return 1 + stage3_query_tables_json() { printf '{"nftables":[{"metainfo":{}},{"table":{"family":"inet","name":"x","handle":7}}]}'; } + stage3_capture_resource_inventory || return 1 + [ "$stage3_inventory_namespaces" = '' ] && [ "$stage3_inventory_links" = '[{"ifname":"lo","ifindex":1}]' ] && [ "$stage3_inventory_tables" = '[{"family":"inet","name":"x","handle":7}]' ] || return 1 + stage3_capture_resource_inventory() { return 1; } + stage3_resources_absent; [ $? -eq 2 ] +} + +case_host_baseline_project_peer_and_schema() { + local addresses routes rules canonical + addresses='[{"ifname":"enp4s0","ifindex":2,"addr_info":[]},{"ifname":"lecodex-host","ifindex":10,"addr_info":[{"family":"inet","local":"10.200.120.1"}]},{"ifname":"lecodex-ns","ifindex":11,"addr_info":[]}]' + canonical=$(stage3_host_addresses_json_canonical "$addresses") || return 1 + printf '%s' "$canonical" | /usr/bin/jq -e 'length == 1 and .[0].ifname == "enp4s0"' >/dev/null || return 1 + routes='[{"dst":"default","dev":"enp4s0"},{"dst":"10.200.120.0/30","dev":"lecodex-host"},{"dst":"10.200.120.0/30","dev":"lecodex-ns"}]' + canonical=$(stage3_host_routes_json_canonical "$routes") || return 1 + printf '%s' "$canonical" | /usr/bin/jq -e 'length == 1 and .[0].dev == "enp4s0"' >/dev/null || return 1 + rules='[{"priority":0}]' + stage3_host_rules_json_canonical "$rules" >/dev/null || return 1 + ! stage3_host_addresses_json_canonical '{}' >/dev/null 2>&1 || return 1 + ! stage3_host_addresses_json_canonical '[null]' >/dev/null 2>&1 || return 1 + ! stage3_host_routes_json_canonical '{}' >/dev/null 2>&1 || return 1 + ! stage3_host_rules_json_canonical '[null]' >/dev/null 2>&1 +} + +case_nft_json_rule_validation() { + local inet nat drift inet_full nat_full fixture_inet_text fixture_nat_text expected_canonical + inet=$(/usr/bin/jq -nc ' + def mm($key; $op; $right): {"match":{"left":{"meta":{"key":$key}},"op":$op,"right":$right}}; + def pm($protocol; $field; $right): {"match":{"left":{"payload":{"protocol":$protocol,"field":$field}},"op":"==","right":$right}}; + def ct: {"match":{"left":{"ct":{"key":"state"}},"op":"in","right":{"set":["established","related"]}}}; + def counter: {"counter":{"packets":0,"bytes":0}}; + def accept: {"accept":null}; def drop: {"drop":null}; + {"nftables":[ + {"rule":{"chain":"input","expr":[mm("iifname";"==";"lecodex-host"),ct,accept]}}, + {"rule":{"chain":"input","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@approved_dns4"),pm("udp";"dport";53),accept]}}, + {"rule":{"chain":"input","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@approved_dns4"),pm("tcp";"dport";53),accept]}}, + {"rule":{"chain":"input","expr":[mm("iifname";"==";"lecodex-host"),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("oifname";"==";"lecodex-host"),ct,accept]}}, + {"rule":{"chain":"forward","expr":[mm("oifname";"==";"lecodex-host"),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@approved_dns4"),pm("udp";"dport";53),accept]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@approved_dns4"),pm("tcp";"dport";53),accept]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@operator_host_public4"),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("ip";"daddr";"@denied4"),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),mm("nfproto";"==";"ipv6"),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("tcp";"dport";{"set":[25,465,587]}),counter,drop]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),pm("tcp";"dport";{"set":[80,443]}),accept]}}, + {"rule":{"chain":"forward","expr":[mm("iifname";"==";"lecodex-host"),counter,drop]}} + ]}') || return 1 + nat=$(/usr/bin/jq -nc '{"nftables":[{"rule":{"chain":"postrouting","expr":[{"match":{"left":{"payload":{"protocol":"ip","field":"saddr"}},"op":"==","right":{"prefix":{"addr":"10.200.120.0","len":30}}}},{"match":{"left":{"meta":{"key":"oifname"}},"op":"!=","right":"lecodex-host"}},{"masquerade":null}]}}]}') || return 1 + stage3_nft_json_inet_rules_match "$inet" || return 1 + stage3_nft_json_nat_rules_match "$nat" || return 1 + drift=$(printf '%s' "$inet" | /usr/bin/jq -c '(.nftables[12].rule.expr[1].match.right.set) += [8080]') || return 1 + ! stage3_nft_json_inet_rules_match "$drift" || return 1 + drift=$(printf '%s' "$nat" | /usr/bin/jq -c '(.nftables[0].rule.expr[1].match.right) = "other0"') || return 1 + ! stage3_nft_json_nat_rules_match "$drift" || return 1 + + inet_full=$(printf '%s' "$inet" | /usr/bin/jq -c ' + .nftables as $rules | + {"nftables": ( + [{"metainfo":{"json_schema_version":1}}, + {"table":{"family":"inet","name":"le_app_codex","handle":10}}, + {"set":{"family":"inet","table":"le_app_codex","name":"approved_dns4","type":"ipv4_addr","handle":11,"flags":["interval"],"elem":["9.9.9.9","149.112.112.112"]}}, + {"set":{"family":"inet","table":"le_app_codex","name":"operator_host_public4","type":"ipv4_addr","handle":12,"flags":["interval"],"elem":["74.67.173.56"]}}, + {"set":{"family":"inet","table":"le_app_codex","name":"denied4","type":"ipv4_addr","handle":13,"flags":["interval"],"elem":["0.0.0.0/8"]}}, + {"chain":{"family":"inet","table":"le_app_codex","name":"input","handle":14,"type":"filter","hook":"input","prio":0,"policy":"accept"}}, + {"chain":{"family":"inet","table":"le_app_codex","name":"forward","handle":15,"type":"filter","hook":"forward","prio":0,"policy":"accept"}}] + + ($rules | map(.rule += {"family":"inet","table":"le_app_codex","handle":100}))) }') || return 1 + nat_full=$(printf '%s' "$nat" | /usr/bin/jq -c ' + .nftables as $rules | + {"nftables": ( + [{"metainfo":{"json_schema_version":1}}, + {"table":{"family":"ip","name":"le_app_codex_nat","handle":20}}, + {"chain":{"family":"ip","table":"le_app_codex_nat","name":"postrouting","handle":21,"type":"nat","hook":"postrouting","prio":100,"policy":"accept"}}] + + ($rules | map(.rule += {"family":"ip","table":"le_app_codex_nat","handle":200}))) }') || return 1 + fixture_inet_text=$'table inet le_app_codex {\n set approved_dns4 {\n type ipv4_addr\n flags interval\n elements = { 9.9.9.9, 149.112.112.112 }\n }\n set operator_host_public4 {\n type ipv4_addr\n flags interval\n elements = { 74.67.173.56 }\n }\n set denied4 {\n type ipv4_addr\n flags interval\n elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }\n }\n chain input { }\n chain forward { }\n}' + fixture_nat_text=$'table ip le_app_codex_nat {\n chain postrouting { }\n}' + stage3_project_inet_snapshot_valid "$inet_full" "$fixture_inet_text" || return 1 + stage3_project_nat_snapshot_valid "$nat_full" "$fixture_nat_text" || return 1 + stage3_query_nft_table_json() { if [ "$1/$2" = inet/le_app_codex ]; then printf '%s' "$inet_full"; else printf '%s' "$nat_full"; fi; } + stage3_query_nft_table_text() { if [ "$1/$2" = inet/le_app_codex ]; then printf '%s' "$fixture_inet_text"; else printf '%s' "$fixture_nat_text"; fi; } + stage3_capture_resource_inventory() { stage3_inventory_namespaces=; stage3_inventory_links='[]'; stage3_inventory_tables='[{"family":"inet","name":"le_app_codex","handle":10},{"family":"ip","name":"le_app_codex_nat","handle":20}]'; } + stage3_read_exact_project_policy_snapshot || return 1 + [ "$stage3_snapshot_inet_handle/$stage3_snapshot_nat_handle" = 10/20 ] || return 1 + expected_canonical=$(stage3_nft_table_json_canonical "$inet_full" inet le_app_codex) || return 1 + [ "$stage3_snapshot_inet_canonical" = "$expected_canonical" ] && [ "$stage3_snapshot_inet_sha256" = "$(printf '%s' "$expected_canonical" | stage3_sha_stream)" ] || return 1 + expected_canonical=$(stage3_nft_table_json_canonical "$nat_full" ip le_app_codex_nat) || return 1 + [ "$stage3_snapshot_nat_canonical" = "$expected_canonical" ] && [ "$stage3_snapshot_nat_sha256" = "$(printf '%s' "$expected_canonical" | stage3_sha_stream)" ] || return 1 + stage3_prepared_table_shape inet le_app_codex || return 1 + stage3_prepared_table_shape ip le_app_codex_nat || return 1 + drift=$(printf '%s' "$inet_full" | /usr/bin/jq -c '.nftables += [{"chain":{"family":"inet","table":"le_app_codex","name":"unexpected","handle":999}}]') || return 1 + ! stage3_project_inet_snapshot_valid "$drift" "$fixture_inet_text" 2>/dev/null || return 1 + drift=$(printf '%s' "$nat_full" | /usr/bin/jq -c '.nftables[0].unexpected = true') || return 1 + ! stage3_project_nat_snapshot_valid "$drift" "$fixture_nat_text" 2>/dev/null +} + +case_stable_project_policy_snapshot() { + local calls=0 stable=$(printf 'a%.0s' {1..64}) drift=$(printf 'b%.0s' {1..64}) + stage3_read_exact_project_policy_snapshot() { + calls=$((calls + 1)) + stage3_snapshot_inet_handle=10 + stage3_snapshot_nat_handle=20 + stage3_snapshot_inet_canonical=inet + stage3_snapshot_nat_canonical=nat + stage3_snapshot_inet_sha256=$stable + stage3_snapshot_nat_sha256=$stable + stage3_snapshot_project_sha256=$stable + [ "$calls" -ne 2 ] || [ "${test_snapshot_drift:-0}" -eq 0 ] || stage3_snapshot_project_sha256=$drift + } + test_snapshot_drift=0 + stage3_capture_exact_project_policy_snapshot || return 1 + calls=0 test_snapshot_drift=1 + ! stage3_capture_exact_project_policy_snapshot +} + +case_nft_digest_query_failures() { + stage3_nft_table_canonical() { return 1; } + stage3_project_nft_canonical() { return 1; } + ! stage3_nft_table_digest inet le_app_codex && ! stage3_project_nft_digest +} + +case_malformed_and_escaping_records() { + local root=$fixture/records good bad + good=$root/good; bad=$root/bad + /usr/bin/mkdir "$root" + write_prepared_record "$good" + stage3_parse_record_file "$good" 0 || return 1 + /usr/bin/cp "$good" "$bad"; printf 'UNKNOWN|x\n' >> "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/cp "$good" "$bad"; printf 'STATUS|PREPARED\n' >> "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/sed 's/^STATUS|PREPARED$/STATUS|PREPARED|/' "$good" > "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/cp "$good" "$bad"; /usr/bin/truncate -s -1 "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/cp "$good" "$bad"; printf '\0' >> "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/sed 's#HOSTNAME|fixture-host#HOSTNAME|../escape#' "$good" > "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + /usr/bin/rm "$bad"; /usr/bin/ln -s good "$bad" + ! stage3_parse_record_file "$bad" 0 || return 1 + ! stage3_read_record "$good" +} + +case_host_and_repository_provenance_mismatch() { + local root=$fixture/provenance good bad head parent original_stage3_root + root=$fixture/provenance; good=$root/good; bad=$root/bad + /usr/bin/mkdir "$root" + head=$(/usr/bin/git -C "$repo_root" rev-parse HEAD) || return 1 + parent=$(/usr/bin/git -C "$repo_root" rev-parse HEAD^) || return 1 + stage3_activation_id=11111111-2222-4333-8444-555555555555 + stage3_machine_id_sha256=$(/usr/bin/sha256sum /etc/machine-id | /usr/bin/cut -d' ' -f1) + stage3_head=$head + stage3_timestamp=2026-07-13T12:00:00Z + stage3_hostname=$(/usr/bin/hostname) + stage3_boot_id=aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee + baseline_public_ipv4=$expected_public_ipv4 + baseline_nft_sha256=$(printf 'c%.0s' {1..64}) + baseline_docker_network_sha256=$(printf 'd%.0s' {1..64}) + baseline_host_network_sha256=$(printf 'e%.0s' {1..64}) + baseline_docker_runtime_sha256=$(printf 'f%.0s' {1..64}) + stage3_render_record PREPARED > "$good"; /usr/bin/chmod 0600 "$good" + stage3_parse_record_file "$good" 1 || return 1 + /usr/bin/sed "s/^HOSTNAME|.*/HOSTNAME|wrong-host/" "$good" > "$bad" + ! stage3_parse_record_file "$bad" 1 || return 1 + /usr/bin/sed "s/^MACHINE_ID_SHA256|.*/MACHINE_ID_SHA256|$(printf '0%.0s' {1..64})/" "$good" > "$bad" + ! stage3_parse_record_file "$bad" 1 || return 1 + /usr/bin/sed "s/^STAGE3_ACTIVATION_COMMIT|.*/STAGE3_ACTIVATION_COMMIT|$(printf '0%.0s' {1..40})/" "$good" > "$bad" + ! stage3_parse_record_file "$bad" 1 || return 1 + original_stage3_root=$stage3_root + stage3_root=$stage2_root + stage3_head=$parent + stage3_render_record PREPARED > "$bad"; /usr/bin/chmod 0600 "$bad" + stage3_head=$head + ! stage3_parse_record_file "$bad" 1 || return 1 + stage3_root=$original_stage3_root +} + +case_lock_mismatch_and_concurrency() { + local root=$fixture/lock wrong=$fixture/wrong uid gid contention token + /usr/bin/mkdir "$root" "$wrong"; /usr/bin/chmod 0755 "$root" "$wrong" + uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + exec 9< "$root" + token=$(/usr/bin/stat -c '%D:%i' "$root") || return 1 + stage3_validate_inherited_lock 9 "$root" "$token" "$uid" "$gid" 755 || return 1 + ! stage3_validate_inherited_lock 9 "$root" 'forged-token' "$uid" "$gid" 755 || return 1 + ! stage3_validate_inherited_lock 9 "$wrong" "$token" "$uid" "$gid" 755 || return 1 + ! stage3_validate_inherited_lock 8 "$root" "$token" "$uid" "$gid" 755 || return 1 + contention=$(ROOT="$root" /bin/bash -c 'exec 7<&-; exec 8< "$ROOT"; /usr/bin/flock -n 8; printf "%s" "$?"') + [ "$contention" -ne 0 ] || return 1 + exec 9<&- +} + +case_transient_collision() { + local free loaded exact unit=le-app-codex-inert-stage3-11111111222243338444555555555555.service activation=11111111-2222-4333-8444-555555555555 token invocation + free=$'LoadState=not-found\nActiveState=inactive\nFragmentPath=' + loaded=$'LoadState=loaded\nActiveState=active\nFragmentPath=/run/systemd/transient/collision.service' + token=$(printf 'a%.0s' {1..32}); invocation=$(printf 'b%.0s' {1..32}) + exact=$(emit_fixture_inert_show "$unit" "$activation" "$token" "$invocation") || return 1 + stage3_inert_unit_unclaimed "$free" && ! stage3_inert_unit_unclaimed "$loaded" || return 1 + stage3_inert_unit_identity "$exact" "$unit" "$invocation" "$activation" "$token" || return 1 + ! stage3_inert_unit_identity "$exact" "$unit" "$invocation" "$activation" "$(printf 'c%.0s' {1..32})" || return 1 + ! stage3_inert_unit_identity "$exact" "$unit" "$(printf 'd%.0s' {1..32})" "$activation" "$token" +} + +case_inert_signal_deferral_and_pid_identity() { + local rc + ( + source "$stage3_root/tests/52-run-stage1-inert.sh" || exit 90 + set +Eeu + inert_pending_signal=0 + stage3_inert_defer_signals + /usr/bin/kill -TERM "$BASHPID" + [ "$inert_pending_signal" -eq 143 ] || exit 91 + stage3_inert_arm_signals + stage3_inert_replay_pending_signal + exit $? + ); rc=$? + [ "$rc" -eq 143 ] || return 1 + ( + source "$stage3_root/tests/52-run-stage1-inert.sh" || exit 90 + set +Eeu + pid=$BASHPID + current=$(stage3_inert_pid_starttime "$pid") || exit 91 + stage3_inert_pid_identity_matches "$pid" "$current" || exit 92 + ! stage3_inert_pid_identity_matches "$pid" "$((current + 1))" + ) +} + +case_inert_probe_cleanup() { + local root=$fixture/probe probe_path uid gid + probe_path=$root/probe + /usr/bin/mkdir "$root"; uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + : > "$probe_path"; /usr/bin/chmod 0644 "$probe_path" + stage3_cleanup_probe_exact "$probe_path" "$uid" "$gid" 644 || return 1 + [ ! -e "$probe_path" ] || return 1 + /usr/bin/ln -s /etc/passwd "$probe_path" + ! stage3_cleanup_probe_exact "$probe_path" "$uid" "$gid" 644 && [ -L "$probe_path" ] +} + +case_inert_probe_failure_trap() { + local root=$fixture/probe-failure probe_path state late uid gid rc + probe_path=$root/probe + state=$root/unit-loaded + late=$root/late-registration + /usr/bin/mkdir "$root"; uid=$(/usr/bin/id -u); gid=$(/usr/bin/id -g) + ( + source "$stage3_root/tests/52-run-stage1-inert.sh" || exit 90 + set +e + inert_probe=$probe_path; inert_probe_uid=$uid; inert_probe_gid=$gid; inert_probe_mode=644 + inert_unit=le-app-codex-inert-stage3-11111111222243338444555555555555.service + inert_activation=11111111-2222-4333-8444-555555555555 + inert_token=$(printf 'a%.0s' {1..32}); inert_invocation=$(printf 'b%.0s' {1..32}) + inert_client_started=1; inert_owned=1; inert_client_pid= + stage3_inert_show() { if [ -e "$late" ]; then /usr/bin/unlink -- "$late"; printf 'LoadState=not-found\n'; elif [ -e "$state" ]; then emit_fixture_inert_show "$inert_unit" "$inert_activation" "$inert_token" "$inert_invocation"; else printf 'LoadState=not-found\n'; fi; } + stage3_inert_stop() { /usr/bin/unlink -- "$state"; } + : > "$state"; : > "$late"; : > "$probe_path"; /usr/bin/chmod 0644 "$probe_path" + trap stage3_inert_cleanup EXIT + exit 37 + ); rc=$? + [ "$rc" -eq 37 ] && [ ! -e "$state" ] && [ ! -e "$probe_path" ] && [ ! -L "$probe_path" ] || return 1 + ( + source "$stage3_root/tests/52-run-stage1-inert.sh" || exit 90 + set +e + inert_probe=$probe_path; inert_probe_uid=$uid; inert_probe_gid=$gid; inert_probe_mode=644 + inert_unit=le-app-codex-inert-stage3-11111111222243338444555555555555.service + inert_activation=11111111-2222-4333-8444-555555555555 + inert_token=$(printf 'a%.0s' {1..32}); inert_invocation=$(printf 'b%.0s' {1..32}) + inert_client_started=1; inert_owned=1; inert_client_pid= + stage3_inert_show() { if [ -e "$state" ]; then emit_fixture_inert_show "$inert_unit" "$inert_activation" "$inert_token" "$inert_invocation"; else printf 'LoadState=not-found\n'; fi; } + stage3_inert_stop() { /usr/bin/unlink -- "$state"; return 1; } + : > "$state"; : > "$probe_path"; /usr/bin/chmod 0644 "$probe_path" + stage3_inert_cleanup + ); rc=$? + [ "$rc" -eq 1 ] && [ ! -e "$state" ] && [ ! -e "$probe_path" ] && [ ! -L "$probe_path" ] +} + +case_docker_gateway_inventory() { + local exact missing extra duplicate + exact=$(stage3_committed_docker_network_canonical) || return 1 + stage3_materialize_docker_ipv4_gateways "$exact" && [ "${#stage3_docker_gateways[@]}" -eq 39 ] || return 1 + missing=$(printf '%s' "$exact" | /usr/bin/jq -c '.[0].IPAM.Config = []') + ! stage3_materialize_docker_ipv4_gateways "$missing" && [ "${#stage3_docker_gateways[@]}" -eq 0 ] || return 1 + extra=$(printf '%s' "$exact" | /usr/bin/jq -c '.[0].IPAM.Config += [{"Subnet":"10.254.0.0/24","Gateway":"10.254.0.1"}]') + ! stage3_materialize_docker_ipv4_gateways "$extra" && [ "${#stage3_docker_gateways[@]}" -eq 0 ] || return 1 + duplicate=$(printf '%s' "$exact" | /usr/bin/jq -c '.[0].IPAM.Config += [.[1].IPAM.Config[0]]') + ! stage3_materialize_docker_ipv4_gateways "$duplicate" && [ "${#stage3_docker_gateways[@]}" -eq 0 ] || return 1 + ! stage3_materialize_docker_ipv4_gateways '[]' && [ "${#stage3_docker_gateways[@]}" -eq 0 ] +} + +case_veth_drift() { + stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 11 aa:bb:cc:dd:ee:ff || return 1 + ! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 12 11 aa:bb:cc:dd:ee:ff || return 1 + ! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 99 aa:bb:cc:dd:ee:ff || return 1 + ! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 11 00:00:00:00:00:00 +} + +case_direct_cleanup_drift_gates() { + stage3_capture_resource_inventory() { return 0; } + stage3_inventory_has_table() { return 0; } + stage3_table_identity() { return 1; } + ! stage3_delete_surviving_table inet le_app_codex || return 1 + stage3_veth_identity() { return 1; } + ! stage3_delete_surviving_veth || return 1 + ! stage3_delete_surviving_namespace +} + +case_recorded_baseline_mismatch() { + local good=$(printf 'a%.0s' {1..64}) + failures=0 + declare -gA stage3_marker_fields=([CURRENT_PUBLIC_NAT_IPV4]="$expected_public_ipv4" [BASELINE_DOCKER_NETWORK_SHA256]="$good" [BASELINE_NFT_SHA256]="$good" [BASELINE_HOST_NETWORK_SHA256]="$good" [BASELINE_DOCKER_RUNTIME_SHA256]="$good") + stage3_public_ipv4() { printf '%s' "$expected_public_ipv4"; } + stage3_docker_network_canonical() { printf x; } + stage3_nonproject_nft_canonical() { printf x; } + stage3_host_network_canonical() { printf x; } + stage3_docker_runtime_canonical() { printf x; } + stage3_sha_stream() { /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1; } + good=$(printf x | stage3_sha_stream) + stage3_marker_fields[BASELINE_DOCKER_NETWORK_SHA256]=$good + stage3_marker_fields[BASELINE_NFT_SHA256]=$good + stage3_marker_fields[BASELINE_HOST_NETWORK_SHA256]=$good + stage3_marker_fields[BASELINE_DOCKER_RUNTIME_SHA256]=$good + stage3_compare_recorded_baseline fixture >/dev/null || return 1 + before=$failures + stage3_marker_fields[BASELINE_NFT_SHA256]=$(printf '0%.0s' {1..64}) + ! stage3_compare_recorded_baseline fixture-drift >/dev/null 2>&1 && [ "$failures" -gt "$before" ] +} + +expect_success 'interrupted marker publication has atomic before/after states' case_marker_publication +expect_success 'marker publication never clobbers an existing target' case_marker_no_clobber +expect_success 'complete interrupted marker temporary is fsynced into retained authority' case_complete_temporary_becomes_durable_authority +expect_success 'interrupted marker temporary accepts empty/full recovery and rejects partial or mismatched state' case_interrupted_marker_temporary +expect_success 'same-status ACTIVE temporary must exactly match retained authority' case_conflicting_active_temporary +expect_success 'interrupted tombstone publication retains one exact authority' case_tombstone_publication +expect_success 'fsynced ACTIVE temporary atomically upgrades retained PREPARED authority' case_durable_active_temporary_upgrade +expect_success 'PREPARED partial activation states retain tombstone through a retry' case_partial_activation_and_rollback_retry +expect_success 'PREPARED tombstone retry remains valid after inactive unit loses live invocation ID' case_prepared_tombstone_retry_without_live_invocation +expect_success 'ACTIVE partial rollback retains tombstone across a second retry and rejects nft drift' case_partial_rollback_retry_and_nft_drift +expect_success 'inventory command, malformed text, and wrong-shape JSON failures are never absence' case_inventory_failures +expect_success 'host baselines exclude both project peers and reject wrong-shape JSON' case_host_baseline_project_peer_and_schema +expect_success 'canonical nft JSON accepts exact ordered rules and rejects semantic drift' case_nft_json_rule_validation +expect_success 'two-pass project policy snapshot rejects an unstable canonical capture' case_stable_project_policy_snapshot +expect_success 'nft canonical query failures cannot become empty-input digests' case_nft_digest_query_failures +expect_success 'malformed, symlink, duplicate, and escaping records rejected' case_malformed_and_escaping_records +expect_success 'host, machine, commit, and package provenance mismatches rejected' case_host_and_repository_provenance_mismatch +expect_success 'actual inherited lock token mismatch and concurrent invocation rejected' case_lock_mismatch_and_concurrency +expect_success 'transient unit collision and wrong ownership token rejected' case_transient_collision +expect_success 'inert signals are deferred through ownership capture and PID start-time identity is exact' case_inert_signal_deferral_and_pid_identity +expect_success 'inert write probe cleaned exactly and symlink preserved' case_inert_probe_cleanup +expect_success 'actual inert cleanup function removes mocked unit/probe and propagates execution or cleanup failure' case_inert_probe_failure_trap +expect_success 'Docker gateway inventory requires exact 39 unique addresses' case_docker_gateway_inventory +expect_success 'veth identity drift rejected before cleanup' case_veth_drift +expect_success 'table/veth drift aborts each direct cleanup path before deletion' case_direct_cleanup_drift_gates +expect_success 'recorded activation-baseline mismatch fails rollback gate' case_recorded_baseline_mismatch + +printf 'Phase 2B Stage 3 failure-path tests failures=%s\n' "$failures" +[ "$failures" -eq 0 ] diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/docker-config/config.json b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/docker-config/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/docker-config/config.json @@ -0,0 +1 @@ +{} diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/rm b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/rm new file mode 100755 index 0000000..0f4ea75 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/rm @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +test "$#" -eq 1 +test "$1" = /srv/le-app-codex/tmp/phase2b-write-test +/usr/bin/unlink /srv/le-app-codex/build-artifacts/.phase2b-stage3-write-test diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/touch b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/touch new file mode 100755 index 0000000..4b60975 --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/inert-bin/touch @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +test "$#" -eq 1 +test "$1" = /srv/le-app-codex/tmp/phase2b-write-test +test ! -e /srv/le-app-codex/build-artifacts/.phase2b-stage3-write-test +test ! -L /srv/le-app-codex/build-artifacts/.phase2b-stage3-write-test +set -C +: > /srv/le-app-codex/build-artifacts/.phase2b-stage3-write-test diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh new file mode 100644 index 0000000..f7ca02d --- /dev/null +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh @@ -0,0 +1,1090 @@ +#!/usr/bin/env bash + +stage3_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd) +migration_root=$(CDPATH= cd -- "$stage3_root/.." 2>/dev/null && pwd) +stage1_root=$migration_root/phase2b-contained-runtime +stage2_root=$migration_root/phase2b-stage2-verification +repo_root=$(CDPATH= cd -- "$migration_root/../.." 2>/dev/null && pwd) +payload_root=$stage1_root/payload +state_parent=/var/lib/le-app-codex-phase2b +state_transaction=$state_parent/stage1 +marker=/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED +marker_temporary=/etc/le-app-codex-runtime/.OPERATOR-INPUTS-APPROVED.stage3-new +rollback_record=/etc/le-app-codex-runtime/.OPERATOR-INPUTS-APPROVED.stage3-rollback +installed_policy=/etc/nftables.d/50-le-app-codex.nft +inert_unit_prefix=le-app-codex-inert-stage3- +inert_probe=/srv/le-app-codex/build-artifacts/.phase2b-stage3-write-test +context_hash=1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535 +context_source=$payload_root/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json +context_destination=/srv/le-app-codex/home/.docker/contexts/meta/$context_hash/meta.json + +expected_stage1_source_commit=df6b53e63916880bec2c77219b04b010b8b453f1 +expected_stage1_artifact_digest=5aaa91b1e6846eda033961dcee392b9657476ad6fd149420979e9309b484445f +expected_stage2_commit=909f0a8200c79efc205d92618be47e06ebf764fc +expected_stage2_manifest_digest=4ce1de182dd6fda902d910c1d6f3f169dce55266e61f9abf2e57cd119fad600d +expected_policy_digest=4289b705dbd786281daccb6d5aa660c27b83441f1a34d0cd18590666124be386 +expected_public_ipv4=74.67.173.56 +approved_dns4=9.9.9.9,149.112.112.112 + +stage3_relative_path_safe() { + local path=$1 + [ -n "$path" ] && [[ "$path" != /* ]] && [[ "$path" != *'|'* ]] && [[ "$path" != *$'\n'* ]] && [[ "$path" != ../* ]] && [[ "$path" != */../* ]] && [[ "$path" != */.. ]] && [ "$path" != . ] && [ "$path" != .. ] +} + +stage3_verify_committed_manifest() { + local package_root=$1 label=$2 package_relative manifest line expected path actual committed rc tracked relative + local record_count=0 tree_count=0 before=$failures + declare -A listed=() + package_relative=${package_root#"$repo_root"/} + manifest=$package_root/MANIFEST.sha256 + if [ -f "$manifest" ] && [ ! -L "$manifest" ]; then pass "$label manifest is a regular file"; else fail "$label manifest missing or unsafe"; return 1; fi + tracked=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-files --error-unmatch -- "$package_relative/MANIFEST.sha256" 2>/dev/null) + if [ "$tracked" = "$package_relative/MANIFEST.sha256" ]; then pass "$label manifest is tracked"; else fail "$label manifest is not tracked"; fi + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^([0-9a-f]{64})\ \ (.+)$ ]]; then expected=${BASH_REMATCH[1]}; path=${BASH_REMATCH[2]}; else fail "$label malformed manifest record"; continue; fi + if ! stage3_relative_path_safe "$path" || [ "$path" = MANIFEST.sha256 ] || [ -n "${listed[$path]:-}" ]; then fail "$label unsafe or duplicate manifest path $path"; continue; fi + listed["$path"]=1 + record_count=$((record_count + 1)) + if [ -f "$package_root/$path" ] && [ ! -L "$package_root/$path" ]; then + actual=$(/usr/bin/sha256sum "$package_root/$path" 2>/dev/null | /usr/bin/cut -d' ' -f1) + [ "$actual" = "$expected" ] && pass "$label working hash $path" || fail "$label working hash mismatch $path" + else + fail "$label working path is missing, non-regular, or a symlink $path" + fi + relative=$package_relative/$path + tracked=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-files --error-unmatch -- "$relative" 2>/dev/null) + if [ "$tracked" = "$relative" ]; then + committed=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" show "HEAD:$relative" 2>/dev/null | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + rc=$? + if [ "$rc" -eq 0 ] && [ "$committed" = "$expected" ]; then pass "$label committed hash $path"; else fail "$label committed hash mismatch $path"; fi + else + fail "$label untracked manifest path $path" + fi + done < "$manifest" + while IFS= read -r tracked; do + relative=${tracked#"$package_relative"/} + [ "$relative" = MANIFEST.sha256 ] && continue + tree_count=$((tree_count + 1)) + [ -n "${listed[$relative]:-}" ] || fail "$label manifest omits tracked path $relative" + done < <(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" ls-tree -r --name-only HEAD -- "$package_relative" 2>/dev/null) + if [ "$record_count" -gt 0 ] && [ "$record_count" -eq "$tree_count" ]; then pass "$label manifest covers $record_count committed files"; else fail "$label manifest/tree count mismatch records=$record_count tree=$tree_count"; fi + [ "$failures" -eq "$before" ] +} + +stage3_verify_repo_provenance() { + local status_rc stage1_relative stage2_relative historical_stage2_digest before=$failures + stage3_head=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" rev-parse HEAD 2>/dev/null) + repo_status=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" status --porcelain --untracked-files=all 2>/dev/null) + status_rc=$? + [[ "$stage3_head" =~ ^[0-9a-f]{40}$ ]] && pass "committed Stage 3 HEAD $stage3_head" || fail 'Stage 3 HEAD unavailable' + [ "$status_rc" -eq 0 ] && [ -z "$repo_status" ] && pass 'repository worktree clean' || fail 'repository worktree dirty or unreadable' + stage3_verify_committed_manifest "$stage1_root" 'Stage 1' || true + stage3_verify_committed_manifest "$stage2_root" 'Stage 2' || true + stage3_verify_committed_manifest "$stage3_root" 'Stage 3' || true + (cd "$stage1_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS >/dev/null) && pass 'Stage 1 inventory checksums' || fail 'Stage 1 inventory checksums' + [ "$(/usr/bin/sha256sum "$stage1_root/MANIFEST.sha256" | /usr/bin/cut -d' ' -f1)" = "$expected_stage1_artifact_digest" ] && pass 'Stage 1 artifact digest pinned' || fail 'Stage 1 artifact digest drift' + [ "$(/usr/bin/sha256sum "$stage2_root/MANIFEST.sha256" | /usr/bin/cut -d' ' -f1)" = "$expected_stage2_manifest_digest" ] && pass 'Stage 2 manifest digest pinned' || fail 'Stage 2 manifest digest drift' + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" cat-file -e "$expected_stage2_commit^{commit}" 2>/dev/null && pass 'Stage 2 verification commit exists' || fail 'Stage 2 verification commit missing' + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" merge-base --is-ancestor "$expected_stage2_commit" HEAD 2>/dev/null && pass 'Stage 2 verification commit is an ancestor of HEAD' || fail 'Stage 2 verification commit is not an ancestor of HEAD' + stage1_relative=${stage1_root#"$repo_root"/} + stage2_relative=${stage2_root#"$repo_root"/} + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" diff --quiet "$expected_stage2_commit" HEAD -- "$stage1_relative" "$stage2_relative" && pass 'Stage 1 and Stage 2 trees unchanged since verification commit' || fail 'Stage 1 or Stage 2 tree changed since verification commit' + historical_stage2_digest=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" show "$expected_stage2_commit:$stage2_relative/MANIFEST.sha256" 2>/dev/null | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + [ "$historical_stage2_digest" = "$expected_stage2_manifest_digest" ] && pass 'Stage 2 commit binds expected package manifest' || fail 'Stage 2 package provenance mismatch' + [ "$failures" -eq "$before" ] +} + +stage3_load_stage1_specs() { + sources=( + "$payload_root/usr/local/libexec/dockerd-rootless-29.6.1.sh" + "$payload_root/usr/local/libexec/le-app-codex-netns" + "$payload_root/etc/le-app-codex-runtime/resolv.conf" + "$payload_root/etc/nftables.d/50-le-app-codex.nft" + "$payload_root/etc/systemd/system/le-app-codex-netns.service" + "$payload_root/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf" + "$payload_root/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf" + "$payload_root/srv/le-app-codex/home/.config/systemd/user/docker.service" + "$payload_root/srv/le-app-codex/home/.config/docker/daemon.json" + "$payload_root/srv/le-app-codex/home/.docker/config.json" + "$context_source" + "$stage1_root/tests/00-read-only-preflight.sh" + "$stage1_root/tests/00-root-preinstall-validate.sh" + "$stage1_root/tests/10-inert-containment.sh" + "$stage1_root/tests/20-rootless-docker.sh" + "$stage1_root/tests/run-inert-without-user-manager.sh" + ) + destinations=( + /usr/local/libexec/dockerd-rootless-29.6.1.sh + /usr/local/libexec/le-app-codex-netns + /etc/le-app-codex-runtime/resolv.conf + /etc/nftables.d/50-le-app-codex.nft + /etc/systemd/system/le-app-codex-netns.service + /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf + /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf + /srv/le-app-codex/home/.config/systemd/user/docker.service + /srv/le-app-codex/home/.config/docker/daemon.json + /srv/le-app-codex/home/.docker/config.json + "$context_destination" + /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh + /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh + /srv/le-app-codex/phase2b-tests/10-inert-containment.sh + /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh + /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh + ) + uids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) + gids=(0 0 0 0 0 0 0 1200 1200 1200 1200 1200 1200 1200 1200 1200) + modes=(0755 0755 0644 0644 0644 0644 0644 0640 0640 0640 0640 0750 0750 0750 0750 0750) + allowed_directories=( + /srv/le-app-codex/home/.docker/contexts/meta/$context_hash /srv/le-app-codex/home/.docker/contexts/meta /srv/le-app-codex/home/.docker/contexts /srv/le-app-codex/home/.docker + /srv/le-app-codex/home/.config/systemd/user /srv/le-app-codex/home/.config/systemd /srv/le-app-codex/home/.config/docker /srv/le-app-codex/home/.config /srv/le-app-codex/phase2b-tests + /etc/systemd/system/user@1200.service.d /etc/systemd/system/user-1200.slice.d /etc/le-app-codex-runtime /etc/nftables.d /usr/local/libexec "$state_parent" + /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc + ) + directory_uids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 0 0 0 0) + directory_gids=(1200 1200 1200 1200 1200 1200 1200 1200 1200 0 0 0 0 0 0 1200 1200 0 0 0) + directory_modes=(0750 0750 0750 0750 0750 0750 0750 0750 0750 0755 0755 0755 0755 0755 0700 0700 0750 0755 0755 0755) + directory_kinds=(project project project project project project project project project project project project shared shared state project-boundary project-boundary shared-base shared-base shared-base) + declare -gA stage1_allowed_file_specs=() stage1_allowed_directory_specs=() stage1_never_created_directories=() + local index source_hash source_size directory + for index in "${!destinations[@]}"; do + source_hash=$(/usr/bin/sha256sum "${sources[$index]}" 2>/dev/null | /usr/bin/cut -d' ' -f1) + source_size=$(/usr/bin/stat -c '%s' "${sources[$index]}" 2>/dev/null) + stage1_allowed_file_specs["${destinations[$index]}"]="$source_hash|$source_size|${uids[$index]}|${gids[$index]}|${modes[$index]}" + done + for index in "${!allowed_directories[@]}"; do stage1_allowed_directory_specs["${allowed_directories[$index]}"]="${directory_uids[$index]}|${directory_gids[$index]}|${directory_modes[$index]}|${directory_kinds[$index]}"; done + for directory in /srv/le-app-codex/home /srv/le-app-codex /usr/local /etc/systemd/system /etc; do stage1_never_created_directories["$directory"]=1; done +} + +stage3_verify_installed_file() { + local path=$1 label=${2:-$1} spec expected_hash expected_size expected_uid expected_gid expected_mode actual_hash actual_size actual_stat links + spec=${stage1_allowed_file_specs[$path]:-} + if [ -z "$spec" ]; then fail "$label has no committed Stage 1 specification"; return 1; fi + IFS='|' read -r expected_hash expected_size expected_uid expected_gid expected_mode <<< "$spec" + actual_hash=$(/usr/bin/sha256sum "$path" 2>/dev/null | /usr/bin/cut -d' ' -f1) + actual_size=$(/usr/bin/stat -c '%s' "$path" 2>/dev/null) + actual_stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + links=$(/usr/bin/stat -c '%h' "$path" 2>/dev/null) + if [ ! -L "$path" ] && [ "$actual_hash" = "$expected_hash" ] && [ "$actual_size" = "$expected_size" ] && [ "$actual_stat" = "$expected_uid:$expected_gid:${expected_mode#0}:regular file" ] && [ "$links" = 1 ]; then pass "$label hash/size/metadata"; else fail "$label hash, size, metadata, type, or link-count mismatch"; return 1; fi +} + +stage3_verify_installed_directory() { + local path=$1 spec expected_uid expected_gid expected_mode expected_kind actual + spec=${stage1_allowed_directory_specs[$path]:-} + if [ -z "$spec" ]; then fail "$path has no committed Stage 1 directory specification"; return 1; fi + IFS='|' read -r expected_uid expected_gid expected_mode expected_kind <<< "$spec" + actual=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null) + if [ -d "$path" ] && [ ! -L "$path" ] && [ "$actual" = "$expected_uid:$expected_gid:${expected_mode#0}:directory" ]; then pass "$path directory metadata ($expected_kind)"; else fail "$path directory metadata, type, or ownership mismatch"; return 1; fi +} + +stage3_verify_lock_parent() { + [ -d /etc ] && [ ! -L /etc ] && [ "$(/usr/bin/stat -c '%u:%g:%a:%F' /etc 2>/dev/null)" = '0:0:755:directory' ] || return 1 + [ -d /etc/le-app-codex-runtime ] && [ ! -L /etc/le-app-codex-runtime ] && [ "$(/usr/bin/stat -c '%u:%g:%a:%F' /etc/le-app-codex-runtime 2>/dev/null)" = '0:0:755:directory' ] +} + +stage3_verify_stage1_state_and_files() { + local destination directory historical_digest completed_directory_records before=$failures + stage3_load_stage1_specs + if [ "${#stage1_allowed_file_specs[@]}" -eq 16 ] && [ "${#stage1_allowed_directory_specs[@]}" -eq 20 ]; then pass 'exact Stage 1 allowlists'; else fail 'Stage 1 allowlist cardinality'; fi + source "$stage1_root/tests/stage1-state-lib.sh" + stage1_state_resolve_current "$state_transaction" 0 0 0600 && pass 'authoritative Stage 1 state generation' || fail 'authoritative Stage 1 state generation' + if [ "$failures" -eq "$before" ]; then stage1_state_validate_records "$stage1_current_manifest" "$expected_stage1_artifact_digest" "$(/usr/bin/hostname)" 16 && pass 'authoritative Stage 1 state records' || fail 'authoritative Stage 1 state records'; fi + completed_directory_records=$((${#stage1_validated_created_directories[@]} + ${#stage1_validated_existing_directories[@]})) + if [ "${stage1_validated_status:-}" = COMPLETE ] && [ "${stage1_validated_source_commit:-}" = "$expected_stage1_source_commit" ] && [ "${stage1_validated_artifact_digest:-}" = "$expected_stage1_artifact_digest" ] && [ "${#stage1_validated_file_paths[@]}" -eq 16 ] && [ "${#stage1_validated_pending_paths[@]}" -eq 0 ] && [ "${#stage1_validated_pending_directories[@]}" -eq 0 ] && [ "$completed_directory_records" -eq 20 ]; then pass 'Stage 1 state exact COMPLETE and pinned'; else fail 'Stage 1 state is not exact pinned COMPLETE'; fi + historical_digest=$(/usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" show "$expected_stage1_source_commit:${stage1_root#"$repo_root"/}/MANIFEST.sha256" 2>/dev/null | /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1) + [ "$historical_digest" = "$expected_stage1_artifact_digest" ] && pass 'Stage 1 state digest binds source commit' || fail 'Stage 1 state/source commit mismatch' + for destination in "${destinations[@]}"; do stage3_verify_installed_file "$destination" || true; done + for directory in "${allowed_directories[@]}"; do stage3_verify_installed_directory "$directory" || true; done + [ "$failures" -eq "$before" ] +} + +stage3_rootful_docker() { + /usr/bin/env -u DOCKER_API_VERSION -u DOCKER_CERT_PATH -u DOCKER_CONTEXT -u DOCKER_HOST -u DOCKER_TLS -u DOCKER_TLS_VERIFY \ + /usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@" +} + +stage3_docker_network_canonical() { + local ids_text raw id + local -a ids=() + ids_text=$(stage3_rootful_docker network ls --no-trunc -q 2>/dev/null) || return 1 + [ -n "$ids_text" ] || return 1 + while IFS= read -r id; do + [[ "$id" =~ ^[0-9a-f]{64}$ ]] || return 1 + ids+=("$id") + done <<< "$ids_text" + [ "${#ids[@]}" -gt 0 ] || return 1 + raw=$(stage3_rootful_docker network inspect --format '{"Name":{{json .Name}},"Id":{{json .Id}},"Scope":{{json .Scope}},"Driver":{{json .Driver}},"EnableIPv4":{{json .EnableIPv4}},"EnableIPv6":{{json .EnableIPv6}},"IPAM":{{json .IPAM}},"Internal":{{json .Internal}},"Attachable":{{json .Attachable}},"Ingress":{{json .Ingress}}}' "${ids[@]}" 2>/dev/null) || return 1 + printf '%s\n' "$raw" | /usr/bin/jq -esS -c ' + if length > 0 and all(.[]; + type == "object" and (.Name | type) == "string" and (.Id | type) == "string" and + (.Scope | type) == "string" and (.Driver | type) == "string" and (.IPAM | type) == "object" and + (.EnableIPv4 | type) == "boolean" and (.EnableIPv6 | type) == "boolean" and + (.Internal | type) == "boolean" and (.Attachable | type) == "boolean" and (.Ingress | type) == "boolean") + then sort_by(.Id) else error("invalid Docker network inventory") end' +} + +stage3_committed_docker_network_canonical() { + /usr/bin/jq -S -c 'map({Name,Id,Scope,Driver,EnableIPv4,EnableIPv6,IPAM,Internal,Attachable,Ingress}) | sort_by(.Id)' "$stage1_root/inventory/docker-networks.json" +} + +stage3_nonproject_nft_canonical() { + local raw + raw=$(/usr/bin/nft -j list ruleset 2>/dev/null) || return 1 + printf '%s' "$raw" | /usr/bin/jq -eS -c ' + def project_object: + to_entries[0] as $e | ($e.value // {}) as $v | + (($e.key == "table" and $v.family == "inet" and $v.name == "le_app_codex") or + ($e.key == "table" and $v.family == "ip" and $v.name == "le_app_codex_nat") or + ($e.key != "table" and $v.family == "inet" and ($v.table // "") == "le_app_codex") or + ($e.key != "table" and $v.family == "ip" and ($v.table // "") == "le_app_codex_nat")); + if type == "object" and (.nftables | type) == "array" and all(.nftables[]; type == "object") + then .nftables |= map(select((has("metainfo") or project_object) | not)) | + walk(if type == "object" and has("counter") and (.counter | type == "object") then .counter |= del(.packets,.bytes) else . end) + else error("invalid nft ruleset JSON") end' +} + +stage3_query_nft_table_json() { /usr/bin/nft -j list table "$1" "$2" 2>/dev/null; } +stage3_query_nft_table_text() { /usr/bin/nft list table "$1" "$2" 2>/dev/null; } + +stage3_nft_table_json_canonical() { + local raw=$1 family=$2 table=$3 + printf '%s' "$raw" | /usr/bin/jq -eS -c --arg family "$family" --arg table "$table" ' + def exact_outer_object: + type == "object" and (keys | length) == 1 and + if has("metainfo") then (.metainfo | type) == "object" + elif has("table") then + (.table | type) == "object" and .table.family == $family and .table.name == $table + elif has("set") then + (.set | type) == "object" and .set.family == $family and .set.table == $table + elif has("chain") then + (.chain | type) == "object" and .chain.family == $family and .chain.table == $table + elif has("rule") then + (.rule | type) == "object" and .rule.family == $family and .rule.table == $table + else false end; + if type == "object" and keys == ["nftables"] and (.nftables | type) == "array" and + (.nftables | length) > 0 and all(.nftables[]; exact_outer_object) and + ([.nftables[] | select(has("metainfo"))] | length) == 1 and + ([.nftables[] | .table? | select(.)] | length) == 1 + then .nftables |= map(select(has("metainfo") | not)) | + walk(if type == "object" and has("counter") and (.counter | type == "object") then .counter |= del(.packets,.bytes) else . end) | + walk(if type == "object" then del(.handle) else . end) + else error("invalid nft table JSON envelope") end' +} + +stage3_nft_table_canonical() { + local family=$1 table=$2 raw + raw=$(stage3_query_nft_table_json "$family" "$table") || return 1 + stage3_nft_table_json_canonical "$raw" "$family" "$table" +} + +stage3_project_nft_canonical_from_tables() { + local inet=$1 nat=$2 + /usr/bin/jq -enS -c --argjson inet "$inet" --argjson nat "$nat" '[$inet,$nat]' +} + +stage3_project_nft_canonical() { + local inet nat + inet=$(stage3_nft_table_canonical inet le_app_codex) || return 1 + nat=$(stage3_nft_table_canonical ip le_app_codex_nat) || return 1 + stage3_project_nft_canonical_from_tables "$inet" "$nat" +} + +stage3_nft_table_digest() { + local canonical + canonical=$(stage3_nft_table_canonical "$1" "$2") || return 1 + printf '%s' "$canonical" | stage3_sha_stream +} + +stage3_project_nft_digest() { + local canonical + canonical=$(stage3_project_nft_canonical) || return 1 + printf '%s' "$canonical" | stage3_sha_stream +} + +stage3_nft_json_inet_rules_match() { + local raw=$1 + printf '%s' "$raw" | /usr/bin/jq -e ' + def rules($chain): [.nftables[] | .rule? | select(.chain == $chain)]; + def meta_match($key; $op; $right): + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"meta":{"key":$key}} and $m.op == $op and $m.right == $right; + def payload_match($protocol; $field; $right): + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"payload":{"protocol":$protocol,"field":$field}} and + ($m.op == "==" or $m.op == "in") and $m.right == $right; + def named_set_match($protocol; $field; $name): payload_match($protocol; $field; "@" + $name); + def anonymous_set_match($protocol; $field; $values): + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"payload":{"protocol":$protocol,"field":$field}} and + ($m.op == "==" or $m.op == "in") and ($m.right == {"set":$values} or $m.right == $values); + def ct_established_related: + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"ct":{"key":"state"}} and ($m.op == "==" or $m.op == "in") and + (($m.right.set? // $m.right) | sort) == ["established","related"]; + def counter: + has("counter") and (.counter | type) == "object" and (.counter | keys | sort) == ["bytes","packets"] and + (.counter.bytes | type) == "number" and (.counter.packets | type) == "number"; + def accept: . == {"accept":null}; + def drop: . == {"drop":null}; + (rules("input")) as $i | (rules("forward")) as $f | + ($i | length) == 4 and ($f | length) == 10 and + ($i[0].expr | length) == 3 and ($i[0].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($i[0].expr[1] | ct_established_related) and ($i[0].expr[2] | accept) and + ($i[1].expr | length) == 4 and ($i[1].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($i[1].expr[1] | named_set_match("ip"; "daddr"; "approved_dns4")) and ($i[1].expr[2] | payload_match("udp"; "dport"; 53)) and ($i[1].expr[3] | accept) and + ($i[2].expr | length) == 4 and ($i[2].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($i[2].expr[1] | named_set_match("ip"; "daddr"; "approved_dns4")) and ($i[2].expr[2] | payload_match("tcp"; "dport"; 53)) and ($i[2].expr[3] | accept) and + ($i[3].expr | length) == 3 and ($i[3].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($i[3].expr[1] | counter) and ($i[3].expr[2] | drop) and + ($f[0].expr | length) == 3 and ($f[0].expr[0] | meta_match("oifname"; "=="; "lecodex-host")) and ($f[0].expr[1] | ct_established_related) and ($f[0].expr[2] | accept) and + ($f[1].expr | length) == 3 and ($f[1].expr[0] | meta_match("oifname"; "=="; "lecodex-host")) and ($f[1].expr[1] | counter) and ($f[1].expr[2] | drop) and + ($f[2].expr | length) == 4 and ($f[2].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[2].expr[1] | named_set_match("ip"; "daddr"; "approved_dns4")) and ($f[2].expr[2] | payload_match("udp"; "dport"; 53)) and ($f[2].expr[3] | accept) and + ($f[3].expr | length) == 4 and ($f[3].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[3].expr[1] | named_set_match("ip"; "daddr"; "approved_dns4")) and ($f[3].expr[2] | payload_match("tcp"; "dport"; 53)) and ($f[3].expr[3] | accept) and + ($f[4].expr | length) == 4 and ($f[4].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[4].expr[1] | named_set_match("ip"; "daddr"; "operator_host_public4")) and ($f[4].expr[2] | counter) and ($f[4].expr[3] | drop) and + ($f[5].expr | length) == 4 and ($f[5].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[5].expr[1] | named_set_match("ip"; "daddr"; "denied4")) and ($f[5].expr[2] | counter) and ($f[5].expr[3] | drop) and + ($f[6].expr | length) == 4 and ($f[6].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[6].expr[1] | meta_match("nfproto"; "=="; "ipv6")) and ($f[6].expr[2] | counter) and ($f[6].expr[3] | drop) and + ($f[7].expr | length) == 4 and ($f[7].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[7].expr[1] | anonymous_set_match("tcp"; "dport"; [25,465,587])) and ($f[7].expr[2] | counter) and ($f[7].expr[3] | drop) and + ($f[8].expr | length) == 3 and ($f[8].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[8].expr[1] | anonymous_set_match("tcp"; "dport"; [80,443])) and ($f[8].expr[2] | accept) and + ($f[9].expr | length) == 3 and ($f[9].expr[0] | meta_match("iifname"; "=="; "lecodex-host")) and ($f[9].expr[1] | counter) and ($f[9].expr[2] | drop) + ' >/dev/null +} + +stage3_nft_json_nat_rules_match() { + local raw=$1 + printf '%s' "$raw" | /usr/bin/jq -e ' + def payload_prefix_match: + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"payload":{"protocol":"ip","field":"saddr"}} and ($m.op == "==" or $m.op == "in") and + ($m.right == {"prefix":{"addr":"10.200.120.0","len":30}} or $m.right == "10.200.120.0/30"); + def oif_not_project: + .match? as $m | + ($m | type) == "object" and ($m | keys | sort) == ["left","op","right"] and + $m.left == {"meta":{"key":"oifname"}} and $m.op == "!=" and $m.right == "lecodex-host"; + def masquerade: has("masquerade") and (.masquerade == null or .masquerade == {}); + [.nftables[] | .rule? | select(.chain == "postrouting")] as $r | + ($r | length) == 1 and ($r[0].expr | length) == 3 and + ($r[0].expr[0] | payload_prefix_match) and ($r[0].expr[1] | oif_not_project) and ($r[0].expr[2] | masquerade) + ' >/dev/null +} + +stage3_expected_denied4() { + printf '%s\n' 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 192.88.99.0/24 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4 | /usr/bin/sort -V +} + +stage3_project_inet_snapshot_valid() { + local raw=$1 text=$2 expected actual + stage3_nft_table_json_canonical "$raw" inet le_app_codex >/dev/null || return 1 + printf '%s' "$raw" | /usr/bin/jq -e ' + ([.nftables[] | .table? | select(.)][0] | + (keys - ["family","handle","name"] | length) == 0) and + ([.nftables[] | .set? | select(.)] as $sets | + ($sets | length) == 3 and + ([$sets[].name] | sort) == ["approved_dns4","denied4","operator_host_public4"] and + all($sets[]; + .family == "inet" and .table == "le_app_codex" and .type == "ipv4_addr" and .flags == ["interval"] and + (keys - ["elem","family","flags","handle","name","table","type"] | length) == 0)) and + ([.nftables[] | .chain? | select(.)] as $chains | + ($chains | length) == 2 and ([$chains[].name] | sort) == ["forward","input"] and + all($chains[]; + .family == "inet" and .table == "le_app_codex" and .type == "filter" and .hook == .name and + .prio == 0 and .policy == "accept" and + (keys - ["family","handle","hook","name","policy","prio","table","type"] | length) == 0)) and + ([.nftables[] | .rule? | select(.)] as $rules | + ($rules | length) == 14 and + ([$rules[] | select(.chain == "input")] | length) == 4 and + ([$rules[] | select(.chain == "forward")] | length) == 10 and + all($rules[]; + .family == "inet" and .table == "le_app_codex" and (.chain == "input" or .chain == "forward") and + (.expr | type) == "array" and + (keys - ["chain","expr","family","handle","table"] | length) == 0)) + ' >/dev/null || return 1 + stage3_nft_json_inet_rules_match "$raw" || return 1 + expected=$'9.9.9.9\n149.112.112.112' + actual=$(stage3_nft_text_set_ipv4_elements "$text" approved_dns4) || return 1 + [ "$actual" = "$expected" ] || return 1 + actual=$(stage3_nft_text_set_ipv4_elements "$text" operator_host_public4) || return 1 + [ "$actual" = "$expected_public_ipv4" ] || return 1 + expected=$(stage3_expected_denied4) || return 1 + actual=$(stage3_nft_text_set_ipv4_elements "$text" denied4) || return 1 + [ "$actual" = "$expected" ] || return 1 + if printf '%s\n' "$text" | /usr/bin/grep -Eiq 'dnat|redirect|prerouting'; then return 1; fi +} + +stage3_project_nat_snapshot_valid() { + local raw=$1 text=$2 + stage3_nft_table_json_canonical "$raw" ip le_app_codex_nat >/dev/null || return 1 + printf '%s' "$raw" | /usr/bin/jq -e ' + ([.nftables[] | .table? | select(.)][0] | + (keys - ["family","handle","name"] | length) == 0) and + ([.nftables[] | .set? | select(.)] | length) == 0 and + ([.nftables[] | .chain? | select(.)] as $chains | + ($chains | length) == 1 and $chains[0].family == "ip" and $chains[0].table == "le_app_codex_nat" and + $chains[0].name == "postrouting" and $chains[0].type == "nat" and $chains[0].hook == "postrouting" and + $chains[0].prio == 100 and $chains[0].policy == "accept" and + ($chains[0] | keys - ["family","handle","hook","name","policy","prio","table","type"] | length) == 0) and + ([.nftables[] | .rule? | select(.)] as $rules | + ($rules | length) == 1 and $rules[0].family == "ip" and $rules[0].table == "le_app_codex_nat" and + $rules[0].chain == "postrouting" and ($rules[0].expr | type) == "array" and + ($rules[0] | keys - ["chain","expr","family","handle","table"] | length) == 0) + ' >/dev/null || return 1 + stage3_nft_json_nat_rules_match "$raw" || return 1 + if printf '%s\n' "$text" | /usr/bin/grep -Eiq 'dnat|redirect|prerouting'; then return 1; fi +} + +stage3_query_namespaces() { /usr/bin/ip netns list 2>/dev/null; } +stage3_query_links_json() { /usr/bin/ip -d -j link show 2>/dev/null; } +stage3_query_tables_json() { /usr/bin/nft -a -j list tables 2>/dev/null; } + +stage3_namespace_inventory_valid() { + /usr/bin/awk ' + NF == 0 { next } + $1 !~ /^[[:alnum:]_.-]+$/ { exit 1 } + seen[$1]++ { exit 1 } + NF == 1 { next } + NF == 3 && $2 == "(id:" && $3 ~ /^[0-9]+\)$/ { next } + { exit 1 } + ' <<< "$1" +} + +stage3_capture_resource_inventory() { + local namespaces raw links tables + stage3_inventory_namespaces= stage3_inventory_links= stage3_inventory_tables= + namespaces=$(stage3_query_namespaces) || return 1 + stage3_namespace_inventory_valid "$namespaces" || return 1 + raw=$(stage3_query_links_json) || return 1 + links=$(printf '%s' "$raw" | /usr/bin/jq -e -c ' + if type == "array" and all(.[]; + type == "object" and (.ifname | type) == "string" and (.ifname | length) > 0 and + (.ifindex | type) == "number") + then . else error("invalid link inventory") end') || return 1 + raw=$(stage3_query_tables_json) || return 1 + tables=$(printf '%s' "$raw" | /usr/bin/jq -e -c ' + if type == "object" and (.nftables | type) == "array" and all(.nftables[]; + type == "object" and + (((keys == ["metainfo"]) and (.metainfo | type) == "object") or + ((keys == ["table"]) and (.table | type) == "object" and + (.table.family | type) == "string" and (.table.name | type) == "string" and + (.table.handle | type) == "number"))) + then [.nftables[] | select(has("table")) | .table | {family,name,handle}] + else error("invalid nft table inventory") end') || return 1 + stage3_inventory_namespaces=$namespaces + stage3_inventory_links=$links + stage3_inventory_tables=$tables +} + +stage3_inventory_has_namespace() { printf '%s\n' "$stage3_inventory_namespaces" | /usr/bin/awk '$1 == "le-app-codex" { found=1 } END { exit !found }'; } +stage3_inventory_has_link() { printf '%s' "$stage3_inventory_links" | /usr/bin/jq -e 'any(.[]; .ifname == "lecodex-host")' >/dev/null; } +stage3_inventory_has_host_ns_peer() { printf '%s' "$stage3_inventory_links" | /usr/bin/jq -e 'any(.[]; .ifname == "lecodex-ns")' >/dev/null; } +stage3_inventory_has_table() { printf '%s' "$stage3_inventory_tables" | /usr/bin/jq -e --arg family "$1" --arg name "$2" 'any(.[]; .family == $family and .name == $name)' >/dev/null; } +stage3_inventory_table_handle() { printf '%s' "$stage3_inventory_tables" | /usr/bin/jq -er --arg family "$1" --arg name "$2" '[.[] | select(.family == $family and .name == $name) | .handle] | select(length == 1) | .[0]'; } + +stage3_read_exact_project_policy_snapshot() { + local before_inet_handle before_nat_handle after_inet_handle after_nat_handle inet_raw nat_raw inet_text nat_text inet_canonical nat_canonical combined + stage3_snapshot_inet_handle= stage3_snapshot_nat_handle= stage3_snapshot_inet_sha256= stage3_snapshot_nat_sha256= stage3_snapshot_project_sha256= + stage3_snapshot_inet_canonical= stage3_snapshot_nat_canonical= + stage3_capture_resource_inventory || return 1 + before_inet_handle=$(stage3_inventory_table_handle inet le_app_codex) || return 1 + before_nat_handle=$(stage3_inventory_table_handle ip le_app_codex_nat) || return 1 + inet_raw=$(stage3_query_nft_table_json inet le_app_codex) || return 1 + nat_raw=$(stage3_query_nft_table_json ip le_app_codex_nat) || return 1 + inet_text=$(stage3_query_nft_table_text inet le_app_codex) || return 1 + nat_text=$(stage3_query_nft_table_text ip le_app_codex_nat) || return 1 + stage3_project_inet_snapshot_valid "$inet_raw" "$inet_text" || return 1 + stage3_project_nat_snapshot_valid "$nat_raw" "$nat_text" || return 1 + inet_canonical=$(stage3_nft_table_json_canonical "$inet_raw" inet le_app_codex) || return 1 + nat_canonical=$(stage3_nft_table_json_canonical "$nat_raw" ip le_app_codex_nat) || return 1 + combined=$(stage3_project_nft_canonical_from_tables "$inet_canonical" "$nat_canonical") || return 1 + stage3_capture_resource_inventory || return 1 + after_inet_handle=$(stage3_inventory_table_handle inet le_app_codex) || return 1 + after_nat_handle=$(stage3_inventory_table_handle ip le_app_codex_nat) || return 1 + [ "$before_inet_handle" = "$after_inet_handle" ] && [ "$before_nat_handle" = "$after_nat_handle" ] || return 1 + stage3_snapshot_inet_handle=$after_inet_handle + stage3_snapshot_nat_handle=$after_nat_handle + stage3_snapshot_inet_canonical=$inet_canonical + stage3_snapshot_nat_canonical=$nat_canonical + stage3_snapshot_inet_sha256=$(printf '%s' "$inet_canonical" | stage3_sha_stream) || return 1 + stage3_snapshot_nat_sha256=$(printf '%s' "$nat_canonical" | stage3_sha_stream) || return 1 + stage3_snapshot_project_sha256=$(printf '%s' "$combined" | stage3_sha_stream) || return 1 + [[ "$stage3_snapshot_inet_handle$stage3_snapshot_nat_handle" =~ ^[0-9]+[0-9]+$ ]] || return 1 + [[ "$stage3_snapshot_inet_sha256$stage3_snapshot_nat_sha256$stage3_snapshot_project_sha256" =~ ^[0-9a-f]{192}$ ]] +} + +stage3_capture_exact_project_policy_snapshot() { + local first_inet_handle first_nat_handle first_inet_canonical first_nat_canonical first_inet_sha256 first_nat_sha256 first_project_sha256 + stage3_read_exact_project_policy_snapshot || return 1 + first_inet_handle=$stage3_snapshot_inet_handle + first_nat_handle=$stage3_snapshot_nat_handle + first_inet_canonical=$stage3_snapshot_inet_canonical + first_nat_canonical=$stage3_snapshot_nat_canonical + first_inet_sha256=$stage3_snapshot_inet_sha256 + first_nat_sha256=$stage3_snapshot_nat_sha256 + first_project_sha256=$stage3_snapshot_project_sha256 + stage3_read_exact_project_policy_snapshot || return 1 + [ "$stage3_snapshot_inet_handle" = "$first_inet_handle" ] && + [ "$stage3_snapshot_nat_handle" = "$first_nat_handle" ] && + [ "$stage3_snapshot_inet_canonical" = "$first_inet_canonical" ] && + [ "$stage3_snapshot_nat_canonical" = "$first_nat_canonical" ] && + [ "$stage3_snapshot_inet_sha256" = "$first_inet_sha256" ] && + [ "$stage3_snapshot_nat_sha256" = "$first_nat_sha256" ] && + [ "$stage3_snapshot_project_sha256" = "$first_project_sha256" ] +} + +stage3_capture_active_topology_identity() { + local namespace_count namespace_stat pids pids_rc host_json ns_json host_addr ns_addr ns_ipv6 routes4 routes6 + local host_ifindex host_iflink host_mac peer_ifindex peer_iflink all_ipv6 default_ipv6 + stage3_active_namespace_dev_inode= stage3_active_host_ifindex= stage3_active_host_iflink= stage3_active_host_mac= + stage3_capture_resource_inventory || return 1 + namespace_count=$(printf '%s\n' "$stage3_inventory_namespaces" | /usr/bin/awk '$1 == "le-app-codex" { count++ } END { print count+0 }') || return 1 + [ "$namespace_count" -eq 1 ] && stage3_inventory_has_link && ! stage3_inventory_has_host_ns_peer || return 1 + [ -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ] || return 1 + [ "$(/usr/bin/stat -f -c '%T' /run/netns/le-app-codex 2>/dev/null)" = nsfs ] || return 1 + namespace_stat=$(/usr/bin/stat -Lc '%D:%i' /run/netns/le-app-codex 2>/dev/null) || return 1 + [[ "$namespace_stat" =~ ^[0-9a-f]+:[0-9]+$ ]] || return 1 + pids=$(/usr/bin/ip netns pids le-app-codex 2>/dev/null); pids_rc=$? + [ "$pids_rc" -eq 0 ] && [ -z "$pids" ] || return 1 + + host_json=$(printf '%s' "$stage3_inventory_links" | /usr/bin/jq -ec '[.[] | select(.ifname == "lecodex-host")] | select(length == 1)') || return 1 + printf '%s' "$host_json" | /usr/bin/jq -e '.[0].operstate == "UP" and .[0].linkinfo.info_kind == "veth"' >/dev/null || return 1 + ns_json=$(/usr/bin/ip -n le-app-codex -d -j link show 2>/dev/null) || return 1 + printf '%s' "$ns_json" | /usr/bin/jq -e ' + type == "array" and length == 2 and all(.[]; type == "object" and (.ifname | type) == "string" and (.ifindex | type) == "number") and + ([.[].ifname] | sort) == ["lecodex-ns","lo"] and + ([.[] | select(.ifname == "lecodex-ns")] | length) == 1 and + ([.[] | select(.ifname == "lecodex-ns")][0] | .operstate == "UP" and .linkinfo.info_kind == "veth")' >/dev/null || return 1 + + host_ifindex=$(< /sys/class/net/lecodex-host/ifindex) || return 1 + host_iflink=$(< /sys/class/net/lecodex-host/iflink) || return 1 + host_mac=$(< /sys/class/net/lecodex-host/address) || return 1 + peer_ifindex=$(/usr/bin/ip netns exec le-app-codex /usr/bin/cat /sys/class/net/lecodex-ns/ifindex 2>/dev/null) || return 1 + peer_iflink=$(/usr/bin/ip netns exec le-app-codex /usr/bin/cat /sys/class/net/lecodex-ns/iflink 2>/dev/null) || return 1 + [[ "$host_ifindex" =~ ^[0-9]+$ ]] && [[ "$host_iflink" =~ ^[0-9]+$ ]] && [[ "$peer_ifindex" =~ ^[0-9]+$ ]] && [[ "$peer_iflink" =~ ^[0-9]+$ ]] || return 1 + [[ "$host_mac" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] || return 1 + [ "$host_ifindex" = "$peer_iflink" ] && [ "$host_iflink" = "$peer_ifindex" ] || return 1 + + host_addr=$(/usr/bin/ip -j -4 address show dev lecodex-host 2>/dev/null) || return 1 + ns_addr=$(/usr/bin/ip -n le-app-codex -j -4 address show dev lecodex-ns 2>/dev/null) || return 1 + printf '%s' "$host_addr" | /usr/bin/jq -e 'type == "array" and length == 1 and ([.[].addr_info[] | select(.family == "inet") | {local,prefixlen}]) == [{"local":"10.200.120.1","prefixlen":30}]' >/dev/null || return 1 + printf '%s' "$ns_addr" | /usr/bin/jq -e 'type == "array" and length == 1 and ([.[].addr_info[] | select(.family == "inet") | {local,prefixlen}]) == [{"local":"10.200.120.2","prefixlen":30}]' >/dev/null || return 1 + routes4=$(/usr/bin/ip -n le-app-codex -j -4 route show table main 2>/dev/null) || return 1 + printf '%s' "$routes4" | /usr/bin/jq -e 'type == "array" and length == 2 and any(.[]; .dst == "10.200.120.0/30" and .dev == "lecodex-ns") and any(.[]; .dst == "default" and .gateway == "10.200.120.1" and .dev == "lecodex-ns")' >/dev/null || return 1 + ns_ipv6=$(/usr/bin/ip -n le-app-codex -j -6 address show 2>/dev/null) || return 1 + routes6=$(/usr/bin/ip -n le-app-codex -j -6 route show table main 2>/dev/null) || return 1 + printf '%s' "$ns_ipv6" | /usr/bin/jq -e 'type == "array" and all(.[]; type == "object") and ([.[].addr_info[]? | select(.family == "inet6")] | length) == 0' >/dev/null || return 1 + printf '%s' "$routes6" | /usr/bin/jq -e 'type == "array" and length == 0' >/dev/null || return 1 + all_ipv6=$(/usr/bin/ip netns exec le-app-codex /usr/bin/sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null) || return 1 + default_ipv6=$(/usr/bin/ip netns exec le-app-codex /usr/bin/sysctl -n net.ipv6.conf.default.disable_ipv6 2>/dev/null) || return 1 + [ "$all_ipv6" = 1 ] && [ "$default_ipv6" = 1 ] || return 1 + + stage3_active_namespace_dev_inode=$namespace_stat + stage3_active_host_ifindex=$host_ifindex + stage3_active_host_iflink=$host_iflink + stage3_active_host_mac=$host_mac +} + +stage3_nft_text_set_ipv4_elements() { + local table_text=$1 set_name=$2 + printf '%s\n' "$table_text" | /usr/bin/awk -v set_name="$set_name" ' + $1 == "set" && $2 == set_name && $3 == "{" { inside=1; next } + inside && $0 ~ /^[[:space:]]*}/ { exit } + inside { print } + ' | /usr/bin/grep -Eo '([0-9]{1,3}[.]){3}[0-9]{1,3}(/[0-9]{1,2})?' | /usr/bin/sort -V +} + +stage3_host_addresses_json_canonical() { + printf '%s' "$1" | /usr/bin/jq -eS -c ' + if type == "array" and all(.[]; + type == "object" and (.ifname | type) == "string" and (.addr_info | type) == "array") + then map(select(.ifname != "lecodex-host" and .ifname != "lecodex-ns")) | + walk(if type == "object" then del(.stats,.stats64,.valid_life_time,.preferred_life_time) else . end) | + sort_by(.ifname) + else error("invalid address inventory") end' +} + +stage3_host_routes_json_canonical() { + printf '%s' "$1" | /usr/bin/jq -eS -c ' + if type == "array" and all(.[]; type == "object") + then map(select((.dev // "") != "lecodex-host" and (.dev // "") != "lecodex-ns")) | + walk(if type == "object" then del(.expires,.used) else . end) | sort_by(tostring) + else error("invalid route inventory") end' +} + +stage3_host_rules_json_canonical() { + printf '%s' "$1" | /usr/bin/jq -eS -c ' + if type == "array" and all(.[]; type == "object") then sort_by(tostring) + else error("invalid rule inventory") end' +} + +stage3_host_network_canonical() { + local raw addresses routes4 routes6 rules4 rules6 namespaces resolver resolver_stat resolver_hash + raw=$(/usr/bin/ip -j address show 2>/dev/null) || return 1 + addresses=$(stage3_host_addresses_json_canonical "$raw") || return 1 + raw=$(/usr/bin/ip -j -4 route show table all 2>/dev/null) || return 1 + routes4=$(stage3_host_routes_json_canonical "$raw") || return 1 + raw=$(/usr/bin/ip -j -6 route show table all 2>/dev/null) || return 1 + routes6=$(stage3_host_routes_json_canonical "$raw") || return 1 + raw=$(/usr/bin/ip -j -4 rule show 2>/dev/null) || return 1 + rules4=$(stage3_host_rules_json_canonical "$raw") || return 1 + raw=$(/usr/bin/ip -j -6 rule show 2>/dev/null) || return 1 + rules6=$(stage3_host_rules_json_canonical "$raw") || return 1 + raw=$(/usr/bin/ip netns list 2>/dev/null) || return 1 + stage3_namespace_inventory_valid "$raw" || return 1 + namespaces=$(printf '%s\n' "$raw" | /usr/bin/awk '$1 != "le-app-codex" { print }' | /usr/bin/sort) || return 1 + resolver_stat=$(/usr/bin/stat -c '%u:%g:%a:%F:%D:%i' /etc/resolv.conf 2>/dev/null) || return 1 + resolver_hash=$(/usr/bin/sha256sum /etc/resolv.conf 2>/dev/null | /usr/bin/cut -d' ' -f1) || return 1 + [[ "$resolver_hash" =~ ^[0-9a-f]{64}$ ]] || return 1 + resolver=$resolver_stat:$resolver_hash + /usr/bin/printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n' "$addresses" "$routes4" "$routes6" "$rules4" "$rules6" "$namespaces" "$resolver" +} + +stage3_docker_runtime_canonical() { + local ids_text raw containers units socket listeners id + local -a ids=() + ids_text=$(stage3_rootful_docker ps --no-trunc -aq 2>/dev/null) || return 1 + if [ -n "$ids_text" ]; then + while IFS= read -r id; do + [[ "$id" =~ ^[0-9a-f]{64}$ ]] || return 1 + ids+=("$id") + done <<< "$ids_text" + raw=$(stage3_rootful_docker inspect --format '{"Id":{{json .Id}},"Name":{{json .Name}},"ImageID":{{json .Image}},"Image":{{json .Config.Image}},"State":{"Status":{{json .State.Status}},"Running":{{json .State.Running}},"Paused":{{json .State.Paused}},"Restarting":{{json .State.Restarting}},"OOMKilled":{{json .State.OOMKilled}},"Dead":{{json .State.Dead}},"Pid":{{json .State.Pid}},"ExitCode":{{json .State.ExitCode}},"StartedAt":{{json .State.StartedAt}},"Health":{{if .State.Health}}{{json .State.Health.Status}}{{else}}null{{end}}},"NetworkMode":{{json .HostConfig.NetworkMode}},"RestartPolicy":{{json .HostConfig.RestartPolicy}},"PortBindings":{{json .HostConfig.PortBindings}},"Ports":{{json .NetworkSettings.Ports}},"Networks":{{json .NetworkSettings.Networks}}}' "${ids[@]}" 2>/dev/null) || return 1 + containers=$(printf '%s\n' "$raw" | /usr/bin/jq -esS -c ' + if length > 0 and all(.[]; + type == "object" and (.Id | type) == "string" and (.Name | type) == "string" and + (.State | type) == "object" and (.Networks | type) == "object") + then sort_by(.Id) else error("invalid Docker runtime inventory") end') || return 1 + else + containers='[]' + fi + units=$(/usr/bin/systemctl show docker.service containerd.service --property=Id --property=LoadState --property=ActiveState --property=SubState --property=MainPID --property=InvocationID --no-pager 2>/dev/null) || return 1 + socket=$(/usr/bin/stat -Lc '%D:%i:%u:%g:%a:%F' /run/docker.sock 2>/dev/null) || return 1 + raw=$(/usr/bin/ss -H -lntup 2>/dev/null) || return 1 + listeners=$(printf '%s\n' "$raw" | /usr/bin/awk 'NF { print $1 "|" $2 "|" $5 "|" $7 }' | /usr/bin/sort) || return 1 + /usr/bin/printf '%s\n%s\n%s\n%s\n' "$containers" "$units" "$socket" "$listeners" +} + +stage3_sha_stream() { /usr/bin/sha256sum | /usr/bin/cut -d' ' -f1; } + +stage3_public_ipv4() { + /usr/bin/env -u ALL_PROXY -u HTTPS_PROXY -u HTTP_PROXY -u NO_PROXY -u all_proxy -u https_proxy -u http_proxy -u no_proxy /usr/bin/curl --noproxy '*' -4fsS --max-time 10 https://api.ipify.org 2>/dev/null +} + +stage3_materialize_docker_ipv4_gateways() { + local current_json=${1:-} committed_json current_list committed_list raw_count unique_count + stage3_docker_gateways=() + [ -n "$current_json" ] || current_json=$(stage3_docker_network_canonical) || return 1 + committed_json=$(stage3_committed_docker_network_canonical) || return 1 + current_list=$(printf '%s' "$current_json" | /usr/bin/jq -er '[.[].IPAM.Config[]? | .Gateway // empty | select(test(":" ) | not)] | sort | .[]') || return 1 + committed_list=$(printf '%s' "$committed_json" | /usr/bin/jq -er '[.[].IPAM.Config[]? | .Gateway // empty | select(test(":" ) | not)] | sort | .[]') || return 1 + raw_count=$(printf '%s\n' "$current_list" | /usr/bin/awk 'NF { count++ } END { print count+0 }') + unique_count=$(printf '%s\n' "$current_list" | /usr/bin/sort -u | /usr/bin/awk 'NF { count++ } END { print count+0 }') + [ "$raw_count" -eq 39 ] && [ "$unique_count" -eq 39 ] && [ "$current_list" = "$committed_list" ] || return 1 + mapfile -t stage3_docker_gateways <<< "$current_list" + [ "${#stage3_docker_gateways[@]}" -eq 39 ] +} + +stage3_capture_baseline() { + local committed_docker current_docker before=$failures + baseline_public_ipv4=$(stage3_public_ipv4) + [ "$baseline_public_ipv4" = "$expected_public_ipv4" ] && pass 'current public/NAT IPv4 pinned' || fail "public/NAT IPv4 drift ${baseline_public_ipv4:-unavailable}" + current_docker=$(stage3_docker_network_canonical) || current_docker= + committed_docker=$(stage3_committed_docker_network_canonical) || committed_docker= + if [ -n "$current_docker" ] && [ "$current_docker" = "$committed_docker" ] && [ "$(printf '%s' "$current_docker" | /usr/bin/jq 'length')" -eq 41 ]; then pass 'current rootful Docker network inventory matches committed 41-network inventory'; else fail 'current rootful Docker network inventory drift'; fi + baseline_docker_network_sha256=$(printf '%s' "$current_docker" | stage3_sha_stream) + baseline_nft_sha256=$(stage3_nonproject_nft_canonical | stage3_sha_stream) || baseline_nft_sha256= + baseline_host_network_sha256=$(stage3_host_network_canonical | stage3_sha_stream) || baseline_host_network_sha256= + baseline_docker_runtime_sha256=$(stage3_docker_runtime_canonical | stage3_sha_stream) || baseline_docker_runtime_sha256= + [[ "$baseline_nft_sha256$baseline_host_network_sha256$baseline_docker_runtime_sha256" =~ ^[0-9a-f]{192}$ ]] && pass 'fresh host/nft/rootful-Docker baselines captured' || fail 'fresh baseline capture failed' + [ "$(/usr/bin/sysctl -n net.ipv4.ip_forward 2>/dev/null)" = 1 ] && pass 'host IPv4 forwarding already enabled' || fail 'host IPv4 forwarding is not already enabled' + /usr/bin/ip -4 -o address show dev enp4s0 2>/dev/null | /usr/bin/grep -qw '192.168.10.151/24' && pass 'current LAN identity' || fail 'current LAN identity drift' + /usr/bin/ip -4 -o address show dev wg0 2>/dev/null | /usr/bin/grep -qw '10.98.0.2/24' && pass 'current WireGuard identity' || fail 'current WireGuard identity drift' + [ "$failures" -eq "$before" ] +} + +stage3_compare_baseline() { + local label=$1 public current before=$failures + public=$(stage3_public_ipv4) + [ "$public" = "$baseline_public_ipv4" ] && pass "$label public/NAT IPv4 unchanged" || fail "$label public/NAT IPv4 changed" + current=$(stage3_docker_network_canonical | stage3_sha_stream) && [ "$current" = "$baseline_docker_network_sha256" ] && pass "$label rootful Docker networks unchanged" || fail "$label rootful Docker network drift" + current=$(stage3_nonproject_nft_canonical | stage3_sha_stream) && [ "$current" = "$baseline_nft_sha256" ] && pass "$label nonproject nftables unchanged" || fail "$label nonproject nftables drift" + current=$(stage3_host_network_canonical | stage3_sha_stream) && [ "$current" = "$baseline_host_network_sha256" ] && pass "$label nonproject host network unchanged" || fail "$label nonproject host network drift" + current=$(stage3_docker_runtime_canonical | stage3_sha_stream) && [ "$current" = "$baseline_docker_runtime_sha256" ] && pass "$label rootful Docker runtime/listeners unchanged" || fail "$label rootful Docker runtime/listener drift" + [ "$failures" -eq "$before" ] +} + +stage3_compare_recorded_baseline() { + local label=$1 public current before=$failures + public=$(stage3_public_ipv4) + [ "$public" = "${stage3_marker_fields[CURRENT_PUBLIC_NAT_IPV4]:-}" ] && pass "$label recorded public/NAT IPv4 unchanged" || fail "$label recorded public/NAT IPv4 drift" + current=$(stage3_docker_network_canonical | stage3_sha_stream) && [ "$current" = "${stage3_marker_fields[BASELINE_DOCKER_NETWORK_SHA256]:-}" ] && pass "$label recorded rootful Docker networks unchanged" || fail "$label recorded rootful Docker network drift" + current=$(stage3_nonproject_nft_canonical | stage3_sha_stream) && [ "$current" = "${stage3_marker_fields[BASELINE_NFT_SHA256]:-}" ] && pass "$label recorded nonproject nftables unchanged" || fail "$label recorded nonproject nftables drift" + current=$(stage3_host_network_canonical | stage3_sha_stream) && [ "$current" = "${stage3_marker_fields[BASELINE_HOST_NETWORK_SHA256]:-}" ] && pass "$label recorded nonproject host network unchanged" || fail "$label recorded nonproject host network drift" + current=$(stage3_docker_runtime_canonical | stage3_sha_stream) && [ "$current" = "${stage3_marker_fields[BASELINE_DOCKER_RUNTIME_SHA256]:-}" ] && pass "$label recorded rootful Docker runtime/listeners unchanged" || fail "$label recorded rootful Docker runtime/listener drift" + [ "$failures" -eq "$before" ] +} + +stage3_verify_no_user_runtime() { + local label=$1 active sub user_show user_rc processes process_rc comms comm_rc codex_count before=$failures + if user_show=$(/usr/bin/systemctl show --no-pager user@1200.service --property=ActiveState --property=SubState 2>/dev/null); then user_rc=0; else user_rc=$?; fi + active=$(stage3_show_value "$user_show" ActiveState 2>/dev/null || true) + sub=$(stage3_show_value "$user_show" SubState 2>/dev/null || true) + [ "$user_rc" -eq 0 ] && [ "$active" = inactive ] && [ "$sub" = dead ] && pass "$label user@1200 inactive/dead" || fail "$label user@1200 query/state rc=$user_rc ${active:-unknown}/${sub:-unknown}" + if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -L /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ] && [ ! -L /var/lib/systemd/linger/1200 ]; then pass "$label lingering absent"; else fail "$label lingering present"; fi + processes=$(/usr/bin/pgrep -u 1200 2>/dev/null); process_rc=$? + [ "$process_rc" -eq 1 ] && [ -z "$processes" ] && pass "$label UID 1200 processes absent" || fail "$label UID 1200 process query/presence rc=$process_rc ${processes:-}" + comms=$(/usr/bin/ps -eo comm= 2>/dev/null); comm_rc=$? + codex_count=$(printf '%s\n' "$comms" | /usr/bin/awk '$1 == "codex" { count++ } END { print count+0 }') + [ "$comm_rc" -eq 0 ] && [ "$codex_count" -eq 0 ] && pass "$label Codex-named processes absent" || fail "$label Codex process inventory failed or found count=$codex_count" + [ ! -e /run/user/1200/docker.sock ] && [ ! -L /run/user/1200/docker.sock ] && pass "$label rootless Docker socket absent" || fail "$label rootless Docker socket present" + [ "$failures" -eq "$before" ] +} + +stage3_show_value() { + local output=$1 property=$2 line value= count=0 + while IFS= read -r line; do + if [[ "$line" = "$property="* ]]; then value=${line#*=}; count=$((count + 1)); fi + done <<< "$output" + [ "$count" -eq 1 ] || return 1 + printf '%s' "$value" +} + +stage3_show_has_word() { + local output=$1 property=$2 expected=$3 value word + value=$(stage3_show_value "$output" "$property") || return 1 + for word in $value; do [ "$word" = "$expected" ] && return 0; done + return 1 +} + +stage3_show_exact() { + local output=$1 property=$2 expected=$3 value + value=$(stage3_show_value "$output" "$property") || return 1 + [ "$value" = "$expected" ] +} + +stage3_show_words_exact() { + local output=$1 property=$2 value actual expected + shift 2 + value=$(stage3_show_value "$output" "$property") || return 1 + actual=$(for word in $value; do printf '%s\n' "$word"; done | /usr/bin/sort) + expected=$(printf '%s\n' "$@" | /usr/bin/sort) + [ "$actual" = "$expected" ] +} + +stage3_inert_unit_identity() { + local output=$1 unit=$2 invocation=${3:-} activation=${4:-} token=${5:-} value + [[ "$token" =~ ^[0-9a-f]{32}$ ]] || return 1 + [ "$(stage3_show_value "$output" LoadState 2>/dev/null)" = loaded ] || return 1 + [ "$(stage3_show_value "$output" Transient 2>/dev/null)" = yes ] || return 1 + [ "$(stage3_show_value "$output" FragmentPath 2>/dev/null)" = "/run/systemd/transient/$unit" ] || return 1 + [ "$(stage3_show_value "$output" User 2>/dev/null)" = 1200 ] && [ "$(stage3_show_value "$output" Group 2>/dev/null)" = 1200 ] || return 1 + [ "$(stage3_show_value "$output" NetworkNamespacePath 2>/dev/null)" = /run/netns/le-app-codex ] || return 1 + [ -z "$activation" ] || [ "$(stage3_show_value "$output" Description 2>/dev/null)" = "Phase 2B Stage 3 inert containment $activation" ] || return 1 + stage3_show_words_exact "$output" UnsetEnvironment ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy || return 1 + stage3_show_words_exact "$output" Environment "DOCKER_HOST=unix:///run/user/1200/docker.sock" "PHASE2B_STAGE3_INERT_TOKEN=$token" || return 1 + value=$(stage3_show_value "$output" ExecStart 2>/dev/null) || return 1 + [[ "$value" = \{\ path=/srv/le-app-codex/phase2b-tests/10-inert-containment.sh\ \;\ argv\[\]=/srv/le-app-codex/phase2b-tests/10-inert-containment.sh\ \;\ ignore_errors=no\ \;* ]] || return 1 + [[ "$value" != *'} ; {'* ]] && [ "$(printf '%s' "$value" | /usr/bin/grep -o 'path=' | /usr/bin/wc -l)" -eq 1 ] || return 1 + value=$(stage3_show_value "$output" InvocationID 2>/dev/null) || return 1 + [[ "$value" =~ ^[0-9a-f]{32}$ ]] || return 1 + [ -z "$invocation" ] || [ "$value" = "$invocation" ] +} + +stage3_inert_unit_unclaimed() { + local output=$1 + stage3_show_exact "$output" LoadState not-found && stage3_show_exact "$output" ActiveState inactive && stage3_show_exact "$output" FragmentPath '' +} + +stage3_veth_fields_match() { + [ "$1" = "$4" ] && [ "$2" = "$5" ] && [ "$3" = "$6" ] +} + +stage3_cleanup_probe_exact() { + local path=$1 expected_uid=${2:-1200} expected_gid=${3:-1200} expected_mode=${4:-644} actual + if [ ! -e "$path" ] && [ ! -L "$path" ]; then return 0; fi + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + actual=$(/usr/bin/stat -c '%u:%g:%a:%h:%s' "$path" 2>/dev/null) || return 1 + [ "$actual" = "$expected_uid:$expected_gid:$expected_mode:1:0" ] || return 1 + /usr/bin/unlink -- "$path" +} + +stage3_verify_loaded_units() { + local mode=${1:-inactive} analyze_output analyze_rc cat_output cat_rc headers netns_show netns_rc user_cat_output user_cat_rc user_headers user_header_set user_show user_rc value active sub before=$failures + [ "$mode" = inactive ] || [ "$mode" = runtime ] || return 1 + analyze_output=$(/usr/bin/systemd-analyze verify --recursive-errors=no le-app-codex-netns.service user@1200.service user-1200.slice 2>&1) + analyze_rc=$? + [ -z "$analyze_output" ] || printf '%s\n' "$analyze_output" + [ "$analyze_rc" -eq 0 ] && [ -z "$analyze_output" ] && pass 'loaded namespace/user units verify with no diagnostics' || fail "loaded namespace/user unit verification rc=$analyze_rc or emitted diagnostics" + + cat_output=$(/usr/bin/systemctl cat --no-pager le-app-codex-netns.service 2>&1); cat_rc=$? + headers=$(printf '%s\n' "$cat_output" | /usr/bin/grep -c '^# /' || true) + if [ "$cat_rc" -eq 0 ] && [ "$headers" -eq 1 ] && printf '%s\n' "$cat_output" | /usr/bin/grep -Fqx '# /etc/systemd/system/le-app-codex-netns.service'; then pass 'namespace unit has exact loaded fragment and no concatenated drop-in'; else fail 'namespace systemctl cat fragment/drop-in surface'; fi + printf '%s\n' "$cat_output" | /usr/bin/grep -Fqx 'ConditionPathExists=/etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED' && pass 'namespace unit approval-marker condition' || fail 'namespace unit approval-marker condition' + printf '%s\n' "$cat_output" | /usr/bin/grep -Fqx 'Before=user@1200.service' && pass 'namespace unit ordering declaration' || fail 'namespace unit ordering declaration' + printf '%s\n' "$cat_output" | /usr/bin/grep -Fqx 'ExecStart=/usr/local/libexec/le-app-codex-netns up' && printf '%s\n' "$cat_output" | /usr/bin/grep -Fqx 'ExecStop=/usr/local/libexec/le-app-codex-netns down' && pass 'namespace unit declared start/stop commands' || fail 'namespace unit declared start/stop commands' + + netns_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=Id --property=LoadState --property=ActiveState --property=SubState --property=UnitFileState --property=FragmentPath --property=DropInPaths --property=NeedDaemonReload --property=Job --property=Type --property=RemainAfterExit --property=ExecStart --property=ExecStop --property=Before 2>&1); netns_rc=$? + [ "$netns_rc" -eq 0 ] || fail "namespace systemctl show rc=$netns_rc" + active=$(stage3_show_value "$netns_show" ActiveState 2>/dev/null); sub=$(stage3_show_value "$netns_show" SubState 2>/dev/null) + if [ "$mode" = inactive ]; then + [ "$(stage3_show_value "$netns_show" Id 2>/dev/null)" = le-app-codex-netns.service ] && [ "$(stage3_show_value "$netns_show" LoadState 2>/dev/null)" = loaded ] && [ "$active" = inactive ] && [ "$sub" = dead ] && [ "$(stage3_show_value "$netns_show" UnitFileState 2>/dev/null)" = static ] && pass 'namespace unit loaded/inactive/dead/static' || fail 'namespace loaded state' + else + [ "$(stage3_show_value "$netns_show" Id 2>/dev/null)" = le-app-codex-netns.service ] && [ "$(stage3_show_value "$netns_show" LoadState 2>/dev/null)" = loaded ] && { [ "$active/$sub" = active/exited ] || [ "$active/$sub" = inactive/dead ] || [ "$active" = activating ] || [ "$active" = failed ]; } && [ "$(stage3_show_value "$netns_show" UnitFileState 2>/dev/null)" = static ] && pass "namespace unit loaded runtime state $active/$sub/static" || fail 'namespace runtime loaded state' + fi + stage3_show_exact "$netns_show" FragmentPath /etc/systemd/system/le-app-codex-netns.service && stage3_show_exact "$netns_show" DropInPaths '' && pass 'namespace loaded fragment exact with no drop-ins' || fail 'namespace fragment or unexpected drop-in' + if [ "$mode" = inactive ]; then + stage3_show_exact "$netns_show" NeedDaemonReload no && stage3_show_exact "$netns_show" Job '' && pass 'namespace manager cache current with no pending job' || fail 'namespace manager cache or pending job' + else + stage3_show_exact "$netns_show" NeedDaemonReload no && stage3_show_exact "$netns_show" Job '' && pass 'namespace manager cache current with no pending job' || fail 'namespace manager cache stale or pending job' + fi + [ "$(stage3_show_value "$netns_show" Type 2>/dev/null)" = oneshot ] && [ "$(stage3_show_value "$netns_show" RemainAfterExit 2>/dev/null)" = yes ] && pass 'namespace service type/retention exact' || fail 'namespace service type/retention' + value=$(stage3_show_value "$netns_show" ExecStart 2>/dev/null) + [[ "$value" = \{\ path=/usr/local/libexec/le-app-codex-netns\ \;\ argv\[\]=/usr/local/libexec/le-app-codex-netns\ up\ \;\ ignore_errors=no\ \;* ]] && [[ "$value" != *'} ; {'* ]] && [ "$(printf '%s' "$value" | /usr/bin/grep -o 'path=' | /usr/bin/wc -l)" -eq 1 ] && pass 'namespace loaded ExecStart exact' || fail 'namespace loaded ExecStart' + value=$(stage3_show_value "$netns_show" ExecStop 2>/dev/null) + [[ "$value" = \{\ path=/usr/local/libexec/le-app-codex-netns\ \;\ argv\[\]=/usr/local/libexec/le-app-codex-netns\ down\ \;\ ignore_errors=no\ \;* ]] && [[ "$value" != *'} ; {'* ]] && [ "$(printf '%s' "$value" | /usr/bin/grep -o 'path=' | /usr/bin/wc -l)" -eq 1 ] && pass 'namespace loaded ExecStop exact' || fail 'namespace loaded ExecStop' + stage3_show_has_word "$netns_show" Before user@1200.service && pass 'namespace loaded Before includes user manager' || fail 'namespace loaded Before ordering' + + user_cat_output=$(/usr/bin/systemctl cat --no-pager user@1200.service 2>&1); user_cat_rc=$? + user_headers=$(printf '%s\n' "$user_cat_output" | /usr/bin/grep -c '^# /' || true) + user_header_set=$(printf '%s\n' "$user_cat_output" | /usr/bin/awk '/^# \// { sub(/^# /, ""); print }' | /usr/bin/sort) + if [ "$user_cat_rc" -eq 0 ] && [ "$user_headers" -eq 3 ] && [ "$user_header_set" = $'/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf\n/usr/lib/systemd/system/user@.service\n/usr/lib/systemd/system/user@.service.d/10-login-barrier.conf' ]; then pass 'user manager has exact loaded fragment and two-drop-in surface'; else fail 'user manager systemctl cat fragment/drop-in surface'; fi + printf '%s\n' "$user_cat_output" | /usr/bin/grep -Fqx 'Requires=le-app-codex-netns.service' && printf '%s\n' "$user_cat_output" | /usr/bin/grep -Fqx 'After=le-app-codex-netns.service' && printf '%s\n' "$user_cat_output" | /usr/bin/grep -Fqx 'NetworkNamespacePath=/run/netns/le-app-codex' && pass 'user manager declared namespace dependency/order/path' || fail 'user manager declared namespace dependency/order/path' + + user_show=$(/usr/bin/systemctl show --no-pager user@1200.service --property=Id --property=LoadState --property=ActiveState --property=SubState --property=UnitFileState --property=FragmentPath --property=DropInPaths --property=NeedDaemonReload --property=Job --property=Type --property=ExecStart --property=Requires --property=After --property=NetworkNamespacePath 2>&1); user_rc=$? + [ "$user_rc" -eq 0 ] || fail "user manager systemctl show rc=$user_rc" + [ "$(stage3_show_value "$user_show" Id 2>/dev/null)" = user@1200.service ] && [ "$(stage3_show_value "$user_show" LoadState 2>/dev/null)" = loaded ] && [ "$(stage3_show_value "$user_show" ActiveState 2>/dev/null)" = inactive ] && [ "$(stage3_show_value "$user_show" SubState 2>/dev/null)" = dead ] && pass 'user manager loaded/inactive/dead' || fail 'user manager loaded state' + stage3_show_exact "$user_show" FragmentPath /usr/lib/systemd/system/user@.service && stage3_show_words_exact "$user_show" DropInPaths /usr/lib/systemd/system/user@.service.d/10-login-barrier.conf /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf && stage3_show_exact "$user_show" NetworkNamespacePath /run/netns/le-app-codex && pass 'user manager exact fragment/drop-ins/namespace' || fail 'user manager fragment, drop-ins, or namespace path' + stage3_show_exact "$user_show" UnitFileState static && stage3_show_exact "$user_show" NeedDaemonReload no && stage3_show_exact "$user_show" Job '' && stage3_show_exact "$user_show" Type notify-reload && pass 'user manager static/type/cache/job state exact' || fail 'user manager static/type/cache/job state' + value=$(stage3_show_value "$user_show" ExecStart 2>/dev/null) + [[ "$value" = \{\ path=/usr/lib/systemd/systemd\ \;\ argv\[\]=/usr/lib/systemd/systemd\ --user\ \;\ ignore_errors=no\ \;* ]] && [[ "$value" != *'} ; {'* ]] && [ "$(printf '%s' "$value" | /usr/bin/grep -o 'path=' | /usr/bin/wc -l)" -eq 1 ] && pass 'user manager loaded ExecStart exact' || fail 'user manager loaded ExecStart' + stage3_show_has_word "$user_show" Requires le-app-codex-netns.service && stage3_show_has_word "$user_show" After le-app-codex-netns.service && pass 'user manager loaded namespace dependency/order' || fail 'user manager namespace dependency/order' + [ "$failures" -eq "$before" ] +} + +stage3_verify_pre_activation_inert() { + local active sub enabled service_show service_rc enabled_rc inert_units inert_rc before=$failures + [ ! -e "$marker" ] && [ ! -L "$marker" ] && [ ! -e "$marker_temporary" ] && [ ! -L "$marker_temporary" ] && [ ! -e "$rollback_record" ] && [ ! -L "$rollback_record" ] && pass 'approval marker, temporary, and rollback record absent' || fail 'marker transaction path present' + if service_show=$(/usr/bin/systemctl show --no-pager le-app-codex-netns.service --property=ActiveState --property=SubState 2>/dev/null); then service_rc=0; else service_rc=$?; fi + active=$(stage3_show_value "$service_show" ActiveState 2>/dev/null || true) + sub=$(stage3_show_value "$service_show" SubState 2>/dev/null || true) + if enabled=$(/usr/bin/systemctl is-enabled le-app-codex-netns.service 2>/dev/null); then enabled_rc=0; else enabled_rc=$?; fi + [ "$service_rc" -eq 0 ] && { [ "$enabled_rc" -eq 0 ] || [ "$enabled_rc" -eq 1 ]; } && [ "$active" = inactive ] && [ "$sub" = dead ] && [ "$enabled" = static ] && pass 'namespace service inactive/dead/static' || fail "namespace service pre-query/state rc=$service_rc/$enabled_rc $active/$sub/$enabled" + if stage3_capture_resource_inventory; then + ! stage3_inventory_has_namespace && [ ! -e /run/netns/le-app-codex ] && [ ! -L /run/netns/le-app-codex ] && pass 'project namespace absent from successful inventory' || fail 'project namespace present' + ! stage3_inventory_has_link && ! stage3_inventory_has_host_ns_peer && pass 'project veth names absent from successful inventory' || fail 'project veth name present' + ! stage3_inventory_has_table inet le_app_codex && ! stage3_inventory_has_table ip le_app_codex_nat && pass 'project nftables tables absent from successful inventory' || fail 'project nftables table present' + else + fail 'pre-activation namespace/link/nft inventory command failed' + fi + inert_units=$(/usr/bin/systemctl list-units --all --plain --no-legend "$inert_unit_prefix*.service" 2>/dev/null); inert_rc=$? + [ "$inert_rc" -eq 0 ] && [ -z "$inert_units" ] && pass 'inert transient unit namespace is unclaimed' || fail 'inert transient unit collision or query failure' + [ ! -e "$inert_probe" ] && [ ! -L "$inert_probe" ] && pass 'inert write-probe path absent' || fail 'inert write-probe path present' + stage3_verify_no_user_runtime 'pre-activation' || true + [ "$failures" -eq "$before" ] +} + +stage3_atomic_publish_new() { + local source=$1 target=$2 source_id target_id + [ -f "$source" ] && [ ! -L "$source" ] && [ ! -e "$target" ] && [ ! -L "$target" ] || return 1 + [ "$(/usr/bin/stat -c '%D' "$source" 2>/dev/null)" = "$(/usr/bin/stat -c '%D' "$(dirname -- "$target")" 2>/dev/null)" ] || return 1 + source_id=$(/usr/bin/stat -c '%D:%i' "$source" 2>/dev/null) || return 1 + /usr/bin/mv --no-clobber -T -- "$source" "$target" || return 1 + [ ! -e "$source" ] && [ ! -L "$source" ] && [ -f "$target" ] && [ ! -L "$target" ] || return 1 + target_id=$(/usr/bin/stat -c '%D:%i' "$target" 2>/dev/null) || return 1 + [ "$target_id" = "$source_id" ] +} + +stage3_sync_parent() { + local path=$1 + /usr/bin/sync -f "$(dirname -- "$path")" 2>/dev/null || /usr/bin/sync +} + +stage3_render_record() { + local status=$1 + printf 'FORMAT|le-app-codex-phase2b-stage3-v2\n' + printf 'STATUS|%s\n' "$status" + printf 'ACTIVATION_ID|%s\n' "$stage3_activation_id" + printf 'MACHINE_ID_SHA256|%s\n' "$stage3_machine_id_sha256" + printf 'APPROVED_DNS4|%s\n' "$approved_dns4" + printf 'APPROVED_ARTIFACT_MANIFEST_SHA256|%s\n' "$expected_stage1_artifact_digest" + printf 'STAGE1_SOURCE_COMMIT|%s\n' "$expected_stage1_source_commit" + printf 'STAGE2_VERIFICATION_COMMIT|%s\n' "$expected_stage2_commit" + printf 'STAGE3_ACTIVATION_COMMIT|%s\n' "$stage3_head" + printf 'TIMESTAMP|%s\n' "$stage3_timestamp" + printf 'HOSTNAME|%s\n' "$stage3_hostname" + printf 'BOOT_ID|%s\n' "$stage3_boot_id" + printf 'CURRENT_PUBLIC_NAT_IPV4|%s\n' "$baseline_public_ipv4" + printf 'POLICY_SHA256|%s\n' "$expected_policy_digest" + printf 'BASELINE_NFT_SHA256|%s\n' "$baseline_nft_sha256" + printf 'BASELINE_DOCKER_NETWORK_SHA256|%s\n' "$baseline_docker_network_sha256" + printf 'BASELINE_HOST_NETWORK_SHA256|%s\n' "$baseline_host_network_sha256" + printf 'BASELINE_DOCKER_RUNTIME_SHA256|%s\n' "$baseline_docker_runtime_sha256" + if [ "$status" = ACTIVE ]; then + printf 'SERVICE_INVOCATION_ID|%s\n' "$stage3_service_invocation_id" + printf 'NAMESPACE_DEV_INODE|%s\n' "$stage3_namespace_dev_inode" + printf 'HOST_VETH_IFINDEX|%s\n' "$stage3_host_veth_ifindex" + printf 'HOST_VETH_IFLINK|%s\n' "$stage3_host_veth_iflink" + printf 'HOST_VETH_MAC|%s\n' "$stage3_host_veth_mac" + printf 'NFT_INET_HANDLE|%s\n' "$stage3_nft_inet_handle" + printf 'NFT_NAT_HANDLE|%s\n' "$stage3_nft_nat_handle" + printf 'NFT_INET_SHA256|%s\n' "$stage3_nft_inet_sha256" + printf 'NFT_NAT_SHA256|%s\n' "$stage3_nft_nat_sha256" + printf 'PROJECT_NFT_SHA256|%s\n' "$stage3_project_nft_sha256" + fi +} + +stage3_record_common_sha256() { + local key + for key in FORMAT ACTIVATION_ID MACHINE_ID_SHA256 APPROVED_DNS4 APPROVED_ARTIFACT_MANIFEST_SHA256 STAGE1_SOURCE_COMMIT STAGE2_VERIFICATION_COMMIT STAGE3_ACTIVATION_COMMIT TIMESTAMP HOSTNAME BOOT_ID CURRENT_PUBLIC_NAT_IPV4 POLICY_SHA256 BASELINE_NFT_SHA256 BASELINE_DOCKER_NETWORK_SHA256 BASELINE_HOST_NETWORK_SHA256 BASELINE_DOCKER_RUNTIME_SHA256; do + [ -n "${stage3_marker_fields[$key]:-}" ] || return 1 + printf '%s|%s\n' "$key" "${stage3_marker_fields[$key]}" + done | stage3_sha_stream +} + +stage3_parse_record_file() { + local LC_ALL=C record=$1 require_provenance=${2:-1} line key value expected_count marker_commit stage3_relative record_size parsed_size=0 + declare -gA stage3_marker_fields=() + [ -f "$record" ] && [ ! -L "$record" ] || return 1 + record_size=$(/usr/bin/stat -c '%s' -- "$record" 2>/dev/null) || return 1 + [[ "$record_size" =~ ^[0-9]+$ ]] && [ "$record_size" -gt 0 ] || return 1 + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] && [[ "$line" != *$'\r'* ]] || return 1 + parsed_size=$((parsed_size + ${#line} + 1)) + [[ "$line" =~ ^([^|]+)\|([^|]+)$ ]] || return 1 + key=${BASH_REMATCH[1]} + value=${BASH_REMATCH[2]} + [[ "$value" != */* ]] && [[ "$value" != *'..'* ]] || return 1 + case "$key" in + FORMAT|STATUS|ACTIVATION_ID|MACHINE_ID_SHA256|APPROVED_DNS4|APPROVED_ARTIFACT_MANIFEST_SHA256|STAGE1_SOURCE_COMMIT|STAGE2_VERIFICATION_COMMIT|STAGE3_ACTIVATION_COMMIT|TIMESTAMP|HOSTNAME|BOOT_ID|CURRENT_PUBLIC_NAT_IPV4|POLICY_SHA256|BASELINE_NFT_SHA256|BASELINE_DOCKER_NETWORK_SHA256|BASELINE_HOST_NETWORK_SHA256|BASELINE_DOCKER_RUNTIME_SHA256|SERVICE_INVOCATION_ID|NAMESPACE_DEV_INODE|HOST_VETH_IFINDEX|HOST_VETH_IFLINK|HOST_VETH_MAC|NFT_INET_HANDLE|NFT_NAT_HANDLE|NFT_INET_SHA256|NFT_NAT_SHA256|PROJECT_NFT_SHA256) ;; + *) return 1 ;; + esac + [ -z "${stage3_marker_fields[$key]:-}" ] || return 1 + stage3_marker_fields["$key"]=$value + done < "$record" + [ "$parsed_size" -eq "$record_size" ] || return 1 + [ "${stage3_marker_fields[FORMAT]:-}" = le-app-codex-phase2b-stage3-v2 ] || return 1 + [ "${stage3_marker_fields[STATUS]:-}" = PREPARED ] || [ "${stage3_marker_fields[STATUS]:-}" = ACTIVE ] || return 1 + [[ "${stage3_marker_fields[ACTIVATION_ID]:-}" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || return 1 + [[ "${stage3_marker_fields[MACHINE_ID_SHA256]:-}" =~ ^[0-9a-f]{64}$ ]] || return 1 + [ "${stage3_marker_fields[APPROVED_DNS4]:-}" = "$approved_dns4" ] || return 1 + [ "${stage3_marker_fields[APPROVED_ARTIFACT_MANIFEST_SHA256]:-}" = "$expected_stage1_artifact_digest" ] || return 1 + [ "${stage3_marker_fields[STAGE1_SOURCE_COMMIT]:-}" = "$expected_stage1_source_commit" ] || return 1 + [ "${stage3_marker_fields[STAGE2_VERIFICATION_COMMIT]:-}" = "$expected_stage2_commit" ] || return 1 + [ "${stage3_marker_fields[POLICY_SHA256]:-}" = "$expected_policy_digest" ] || return 1 + [ "${stage3_marker_fields[CURRENT_PUBLIC_NAT_IPV4]:-}" = "$expected_public_ipv4" ] || return 1 + [[ "${stage3_marker_fields[TIMESTAMP]:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || return 1 + [[ "${stage3_marker_fields[STAGE3_ACTIVATION_COMMIT]:-}" =~ ^[0-9a-f]{40}$ ]] || return 1 + [[ "${stage3_marker_fields[BOOT_ID]:-}" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || return 1 + [[ "${stage3_marker_fields[BASELINE_NFT_SHA256]:-}${stage3_marker_fields[BASELINE_DOCKER_NETWORK_SHA256]:-}${stage3_marker_fields[BASELINE_HOST_NETWORK_SHA256]:-}${stage3_marker_fields[BASELINE_DOCKER_RUNTIME_SHA256]:-}" =~ ^[0-9a-f]{256}$ ]] || return 1 + if [ "${stage3_marker_fields[STATUS]}" = ACTIVE ]; then + expected_count=28 + [[ "${stage3_marker_fields[SERVICE_INVOCATION_ID]:-}" =~ ^[0-9a-f]{32}$ ]] || return 1 + [[ "${stage3_marker_fields[NAMESPACE_DEV_INODE]:-}" =~ ^[0-9a-f]+:[0-9]+$ ]] || return 1 + [[ "${stage3_marker_fields[HOST_VETH_IFINDEX]:-}" =~ ^[0-9]+$ ]] || return 1 + [[ "${stage3_marker_fields[HOST_VETH_IFLINK]:-}" =~ ^[0-9]+$ ]] || return 1 + [[ "${stage3_marker_fields[HOST_VETH_MAC]:-}" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] || return 1 + [[ "${stage3_marker_fields[NFT_INET_HANDLE]:-}" =~ ^[0-9]+$ ]] || return 1 + [[ "${stage3_marker_fields[NFT_NAT_HANDLE]:-}" =~ ^[0-9]+$ ]] || return 1 + [[ "${stage3_marker_fields[NFT_INET_SHA256]:-}${stage3_marker_fields[NFT_NAT_SHA256]:-}${stage3_marker_fields[PROJECT_NFT_SHA256]:-}" =~ ^[0-9a-f]{192}$ ]] || return 1 + else + expected_count=18 + fi + [ "${#stage3_marker_fields[@]}" -eq "$expected_count" ] || return 1 + [ "$require_provenance" -eq 1 ] || return 0 + [ "${stage3_marker_fields[HOSTNAME]:-}" = "$(/usr/bin/hostname)" ] || return 1 + [ "${stage3_marker_fields[MACHINE_ID_SHA256]}" = "$(/usr/bin/sha256sum /etc/machine-id 2>/dev/null | /usr/bin/cut -d' ' -f1)" ] || return 1 + marker_commit=${stage3_marker_fields[STAGE3_ACTIVATION_COMMIT]} + stage3_relative=${stage3_root#"$repo_root"/} + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" cat-file -e "$marker_commit^{commit}" 2>/dev/null || return 1 + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" merge-base --is-ancestor "$marker_commit" "${stage3_head:-HEAD}" 2>/dev/null || return 1 + /usr/bin/git -c safe.directory="$repo_root" -C "$repo_root" diff --quiet "$marker_commit" "${stage3_head:-HEAD}" -- "$stage3_relative" || return 1 +} + +stage3_read_record() { + local record=$1 stat links + [ "$record" = "$marker" ] || [ "$record" = "$rollback_record" ] || return 1 + stat=$(/usr/bin/stat -c '%u:%g:%a:%F' "$record" 2>/dev/null) + links=$(/usr/bin/stat -c '%h' "$record" 2>/dev/null) + [ "$stat" = '0:0:600:regular file' ] && [ "$links" = 1 ] || return 1 + stage3_parse_record_file "$record" 1 +} + +stage3_read_marker() { stage3_read_record "$marker"; } +stage3_read_rollback_record() { stage3_read_record "$rollback_record"; } + +stage3_write_marker() { + local status=$1 publish_mode=create old_activation= old_hash temporary_id + [ "$status" = PREPARED ] || [ "$status" = ACTIVE ] || return 1 + stage3_verify_lock_parent || return 1 + [ ! -e "$marker_temporary" ] && [ ! -L "$marker_temporary" ] && [ ! -e "$rollback_record" ] && [ ! -L "$rollback_record" ] || return 1 + if [ "$status" = PREPARED ]; then + [ ! -e "$marker" ] && [ ! -L "$marker" ] || return 1 + else + stage3_read_marker || return 1 + [ "${stage3_marker_fields[STATUS]}" = PREPARED ] || return 1 + old_activation=${stage3_marker_fields[ACTIVATION_ID]} + old_hash=$(/usr/bin/sha256sum "$marker" | /usr/bin/cut -d' ' -f1) || return 1 + publish_mode=replace + fi + /usr/bin/install -o 0 -g 0 -m 0600 -T /dev/null "$marker_temporary" || return 1 + stage3_render_record "$status" > "$marker_temporary" || return 1 + /usr/bin/chown 0:0 "$marker_temporary" && /usr/bin/chmod 0600 "$marker_temporary" || return 1 + [ "$(/usr/bin/stat -c '%u:%g:%a:%F:%h' "$marker_temporary" 2>/dev/null)" = '0:0:600:regular file:1' ] || return 1 + stage3_parse_record_file "$marker_temporary" 1 || return 1 + [ -z "$old_activation" ] || [ "${stage3_marker_fields[ACTIVATION_ID]}" = "$old_activation" ] || return 1 + /usr/bin/sync -f "$marker_temporary" 2>/dev/null || /usr/bin/sync || return 1 + temporary_id=$(/usr/bin/stat -c '%D:%i' "$marker_temporary" 2>/dev/null) || return 1 + if [ "$publish_mode" = create ]; then + stage3_atomic_publish_new "$marker_temporary" "$marker" || return 1 + else + stage3_read_marker || return 1 + [ "${stage3_marker_fields[STATUS]}" = PREPARED ] && [ "${stage3_marker_fields[ACTIVATION_ID]}" = "$old_activation" ] || return 1 + [ "$old_hash" = "$(/usr/bin/sha256sum "$marker" 2>/dev/null | /usr/bin/cut -d' ' -f1)" ] || return 1 + /usr/bin/mv -T -- "$marker_temporary" "$marker" || return 1 + [ ! -e "$marker_temporary" ] && [ "$(/usr/bin/stat -c '%D:%i' "$marker" 2>/dev/null)" = "$temporary_id" ] || return 1 + fi + stage3_sync_parent "$marker" || return 1 + stage3_read_marker && [ "${stage3_marker_fields[STATUS]}" = "$status" ] +} + +stage3_promote_marker_to_tombstone() { + local marker_id + stage3_verify_lock_parent || return 1 + [ ! -e "$rollback_record" ] && [ ! -L "$rollback_record" ] || return 1 + stage3_read_marker || return 1 + marker_id=$(/usr/bin/stat -c '%D:%i' "$marker" 2>/dev/null) || return 1 + stage3_atomic_publish_new "$marker" "$rollback_record" || return 1 + [ "$(/usr/bin/stat -c '%D:%i' "$rollback_record" 2>/dev/null)" = "$marker_id" ] || return 1 + stage3_sync_parent "$rollback_record" || return 1 + stage3_read_rollback_record +} + +stage3_lock_descriptor_matches() { + local fd=$1 path=$2 expected_uid=${3:-0} expected_gid=${4:-0} expected_mode=${5:-755} fd_stat path_stat + [[ "$fd" =~ ^[0-9]+$ ]] && [ -e "/proc/self/fd/$fd" ] || return 1 + fd_stat=$(/usr/bin/stat -Lc '%D:%i:%u:%g:%a:%F' "/proc/self/fd/$fd" 2>/dev/null) || return 1 + path_stat=$(/usr/bin/stat -c '%D:%i:%u:%g:%a:%F' "$path" 2>/dev/null) || return 1 + [ "$fd_stat" = "$path_stat" ] && [[ "$path_stat" = *":$expected_uid:$expected_gid:$expected_mode:directory" ]] +} + +stage3_validate_inherited_lock() { + local fd=$1 path=$2 token=$3 expected_uid=${4:-0} expected_gid=${5:-0} expected_mode=${6:-755} current_id + stage3_lock_descriptor_matches "$fd" "$path" "$expected_uid" "$expected_gid" "$expected_mode" || return 1 + current_id=$(/usr/bin/stat -c '%D:%i' "$path" 2>/dev/null) || return 1 + [ "$token" = "$current_id" ] || return 1 + /usr/bin/flock -n "$fd" || return 1 + stage3_lock_descriptor_matches "$fd" "$path" "$expected_uid" "$expected_gid" "$expected_mode" || return 1 + [ "$current_id" = "$(/usr/bin/stat -c '%D:%i' "$path" 2>/dev/null)" ] +} + +stage3_acquire_lock() { + local current_id + stage3_verify_lock_parent || return 1 + if [ "${PHASE2B_STAGE3_LOCK_HELD:-0}" = 1 ]; then + stage3_validate_inherited_lock 9 /etc/le-app-codex-runtime "${PHASE2B_STAGE3_LOCK_ID:-}" || return 1 + else + exec 9< /etc/le-app-codex-runtime || return 1 + stage3_lock_descriptor_matches 9 /etc/le-app-codex-runtime || return 1 + /usr/bin/flock -n 9 || return 1 + stage3_lock_descriptor_matches 9 /etc/le-app-codex-runtime || return 1 + current_id=$(/usr/bin/stat -c '%D:%i' /etc/le-app-codex-runtime 2>/dev/null) || return 1 + PHASE2B_STAGE3_LOCK_ID=$current_id + fi +} From 1f5caf87611d131f35e7197684a75364a2c5c9e1 Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 14 Jul 2026 00:14:35 -0400 Subject: [PATCH 14/14] ignore ephemeral Docker network identity in Stage 3 --- .../MANIFEST.sha256 | 8 +-- .../README.md | 2 + .../tests/53-stage3-static-tests.sh | 10 ++- .../tests/54-stage3-failure-path-tests.sh | 71 +++++++++++++++++-- .../tests/stage3-common.sh | 66 ++++++++++++++--- 5 files changed, 138 insertions(+), 19 deletions(-) diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 index 26b8d77..6f61133 100644 --- a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/MANIFEST.sha256 @@ -1,11 +1,11 @@ -1e281c255a9a7ba72734c40a947cab4168f70b7e6b733eba7415109a40776a9d README.md +833809d6ce01da516fda0873d80cdbddcf6900c6aaf00be2a24201e8887c76f1 README.md 908ab54049c1e810ba5c47812fc51848e9eef8818a9ddc373f841672ab509a57 ROLLBACK.md 136707ac1391bd241efc834c549c0f54d4a849ce832c55457bdeb243cc03800a tests/50-activate-stage3.sh 5c8eceb9b80865ae09bd4ece2a248da4677237abcc7c55d26334020543e8edf8 tests/51-rollback-stage3.sh 9aa43f4b72057ea28ec00309889f75e756a9b36db77e5db758b752990768c0a0 tests/52-run-stage1-inert.sh -de70e85a4e4e6015726dfe3cbad2bdccb2990daae25eeeab272e107d2d68a115 tests/53-stage3-static-tests.sh -ec595556f2842329a8b1b6e88bc291ef8f4d0d98e675baeb4b317da6ae1c8e96 tests/54-stage3-failure-path-tests.sh +9e8ebfc21c026f7bfde07a9c4f7c190155060e52c2a792e654891a34855de84f tests/53-stage3-static-tests.sh +575b8ab40811daf66ba6602ff2b623acbcfc20e67b909c4f4d61f37aa3fc1790 tests/54-stage3-failure-path-tests.sh ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 tests/docker-config/config.json 1c64518e42afdf490047358c523a8213e989a84a61f51ff0126439db2b485fe3 tests/inert-bin/rm 034b23c09a023ce94d8b7888f0d6f45aed31ba14dbed2efd30c4b0a6322f657b tests/inert-bin/touch -3a3a84df1021e02cc258edbee621ab8abfb9f83bdc17dfa689fb9823b17c8cfd tests/stage3-common.sh +95a5241890b841f5838dc37b364f27d8c00eb0324e718a615946d14d946bbaba tests/stage3-common.sh diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md index 5c2dc9e..e448604 100644 --- a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/README.md @@ -19,6 +19,8 @@ Before any mutation, activation requires: - fresh immediate fingerprints of nonproject nftables, stable nonproject host network state, rootful Docker networks/runtime/ports, host listeners, resolver metadata, and public IPv4; and - no marker, namespace, project veth/table, user manager, linger, UID-1200 process, or rootless socket. +The 41-network check uses one deterministic, name-sorted policy canonical for committed preflight, fresh-baseline capture, activation provenance, automatic recovery, and explicit rollback. It requires the exact unique name set and count; driver, scope, `Internal`, `Attachable`, `Ingress`, `EnableIPv6`, and `ConfigOnly`; normalized IPAM driver/options and every subnet, gateway, IP range, and auxiliary address; and network driver options that affect routing or isolation. Docker-generated network IDs, creation timestamps, container/endpoint attachments, peers, status, and other runtime bookkeeping are excluded. The separate denial gate still requires the exact committed set of 39 unique IPv4 Docker gateways. + Immediately before publication and start, the script revalidates the loaded manager state: `systemd-analyze verify`; exact `systemctl cat` fragment/drop-in surfaces for both units; exact namespace fragment path with no drop-ins; current manager cache; no pending job; exact single `ExecStart`/`ExecStop`; oneshot/`RemainAfterExit`; static inactive state; the approval-marker condition; `Before=user@1200.service`; and the reciprocal user-manager `Requires`/`After`, namespace path, fragment, and exact two-drop-in set. The script renders and fsyncs a root-owned mode `0600`, single-link version-2 record before atomically renaming it into the approval-marker path and syncing the parent directory. No hard-link publication window exists, and an interrupted write cannot replace the prior authoritative record. Parsing requires the exact canonical one-delimiter, final-newline-complete byte representation, so trailing fields, truncated final writes, and embedded NUL bytes are rejected. The immutable fields include an activation UUID, machine-ID digest, approved Quad9 pair, Stage 1 source and artifact digest, Stage 2 verification commit, Stage 3 activation commit, UTC timestamp, hostname, boot ID, public IPv4, installed-policy digest, and baseline fingerprints. It starts `le-app-codex-netns.service`, then atomically replaces `PREPARED` with an `ACTIVE` record bound to the service invocation, namespace inode, host-veth ifindex/iflink/MAC, both nft table handles, independent counter-normalized table digests, and the combined project-policy digest. diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh index 9b0cbfa..f641a38 100644 --- a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/53-stage3-static-tests.sh @@ -76,6 +76,7 @@ if /usr/bin/grep -Eq '/usr/bin/nft[[:space:]]+--numeric|/usr/bin/nft[[:space:]]+ [ "$(/usr/bin/grep -Fc 'stage3_nft_json_inet_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_nft_json_nat_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_inet_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nat_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_capture_exact_project_policy_snapshot' "$activate")" -eq 4 ] && ! /usr/bin/grep -q 'stage3_nft_text_chain' "$activate" "$rollback" "$common" && pass 'activation and rollback share exact canonical JSON policy validation' || fail 'nft JSON validation surface' [ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table inet le_app_codex' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table ip le_app_codex_nat' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip link delete lecodex-host' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip netns delete le-app-codex' "$rollback")" -eq 1 ] && pass 'rollback direct cleanup limited to exact project names' || fail 'rollback direct cleanup literals' if [ "$(/usr/bin/grep -Fc '/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/docker' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ' "$common")" -eq 4 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network ls --no-trunc -q' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ps --no-trunc -aq' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '-u DOCKER_API_VERSION -u DOCKER_CERT_PATH -u DOCKER_CONTEXT -u DOCKER_HOST -u DOCKER_TLS -u DOCKER_TLS_VERIFY' "$common")" -eq 1 ] && /usr/bin/jq -e 'type == "object" and length == 0' "$stage3_root/tests/docker-config/config.json" >/dev/null; then pass 'all rootful Docker reads use one context-scrubbed explicit-socket read allowlist and empty config'; else fail 'rootful Docker helper/socket/config surface'; fi +[ "$(/usr/bin/grep -Fc 'stage3_docker_network_policy_canonical' "$common")" -eq 3 ] && [ "$(/usr/bin/grep -Fc '}) | sort_by(.Name)' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_docker_network_policy_canonical < "$stage1_root/inventory/docker-networks.json"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "case_docker_network_policy_canonical" "$failure_suite")" -eq 2 ] && pass 'one name-sorted Docker policy canonical feeds live, committed, baseline, and rollback comparisons' || fail 'Docker network policy canonical surface' [ "$(/usr/bin/grep -Fc '/usr/bin/systemd-run --service-type=exec --wait --pipe --collect --unit="$inert_unit"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/srv/le-app-codex/phase2b-tests/10-inert-containment.sh' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '--property=RuntimeMaxSec=120s' "$runner")" -eq 1 ] && pass 'finite collected wrapper executes existing installed inert test' || fail 'inert test wrapper command surface' [ "$(/usr/bin/grep -Fc -- "--property='UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy'" "$runner")" -eq 1 ] && ! /usr/bin/grep -Eq -- '--setenv=(ALL_PROXY|HTTPS_PROXY|HTTP_PROXY|NO_PROXY|all_proxy|https_proxy|http_proxy|no_proxy)' "$runner" && pass 'transient inert test removes every proxy variable' || fail 'transient inert proxy environment' [ "$(/usr/bin/grep -Fc -- '--setenv="PHASE2B_STAGE3_INERT_TOKEN=$inert_token"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$inert_owned" -eq 1 ] && [[ "$inert_invocation" =~ ^[0-9a-f]{32}$ ]]' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_unit_identity "$show" "$inert_unit" "${inert_invocation:-}" "$inert_activation" "$inert_token"' "$runner")" -eq 1 ] && pass 'transient success and cleanup require token-bound captured ownership' || fail 'transient ownership/token gate' @@ -112,7 +113,14 @@ if /usr/bin/awk ' failures_before=$failures source "$common" committed_networks=$(stage3_committed_docker_network_canonical) -printf '%s' "$committed_networks" | /usr/bin/jq -e 'length == 41' >/dev/null && pass 'committed Docker canonicalizer yields 41 networks' || fail 'committed Docker canonicalizer' +printf '%s' "$committed_networks" | /usr/bin/jq -e ' + length == 41 and ([.[].Name] | unique | length) == 41 and + all(.[]; + keys == ["Attachable","ConfigOnly","Driver","EnableIPv6","IPAM","Ingress","Internal","Name","Options","Scope"] and + (.Options | type) == "object" and + (.IPAM | keys) == ["Config","Driver","Options"] and + (.IPAM.Options | type) == "object" and (.IPAM.Config | type) == "array" and + all(.IPAM.Config[]; keys == ["AuxiliaryAddresses","Gateway","IPRange","Subnet"] and (.AuxiliaryAddresses | type) == "object"))' >/dev/null && pass 'committed Docker policy canonical yields exact 41-name inventory' || fail 'committed Docker policy canonical' [ "$failures" -eq "$failures_before" ] || fail 'common library source has side effects' /bin/bash "$stage1_root/tests/32-stage1-state-tests.sh" >/dev/null && pass 'existing non-mutating Stage 1 state tests' || fail 'Stage 1 state tests' diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh index 75b3ff1..2396f80 100644 --- a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/54-stage3-failure-path-tests.sh @@ -418,7 +418,7 @@ case_malformed_and_escaping_records() { } case_host_and_repository_provenance_mismatch() { - local root=$fixture/provenance good bad head parent original_stage3_root + local root=$fixture/provenance good bad head parent root=$fixture/provenance; good=$root/good; bad=$root/bad /usr/bin/mkdir "$root" head=$(/usr/bin/git -C "$repo_root" rev-parse HEAD) || return 1 @@ -442,13 +442,10 @@ case_host_and_repository_provenance_mismatch() { ! stage3_parse_record_file "$bad" 1 || return 1 /usr/bin/sed "s/^STAGE3_ACTIVATION_COMMIT|.*/STAGE3_ACTIVATION_COMMIT|$(printf '0%.0s' {1..40})/" "$good" > "$bad" ! stage3_parse_record_file "$bad" 1 || return 1 - original_stage3_root=$stage3_root - stage3_root=$stage2_root stage3_head=$parent stage3_render_record PREPARED > "$bad"; /usr/bin/chmod 0600 "$bad" stage3_head=$head ! stage3_parse_record_file "$bad" 1 || return 1 - stage3_root=$original_stage3_root } case_lock_mismatch_and_concurrency() { @@ -563,6 +560,71 @@ case_docker_gateway_inventory() { ! stage3_materialize_docker_ipv4_gateways '[]' && [ "${#stage3_docker_gateways[@]}" -eq 0 ] } +case_docker_network_policy_canonical() { + local raw baseline changed alternate canonical alternate_canonical filter + local old_id=4ecbee63f4b0f705479ec65b74b57c618b61d76da91bc5c042717460709dadb8 + local new_id=575a3269b7cf00a0e79b3e3f1e2a7c5b8159b125792704b2f6044a38658493d4 + local -a policy_mutations=( + 'map(if .Name == "immich_s6_net" then .Name = "immich_s6_net_changed" else . end)' + 'map(select(.Name != "immich_s6_net"))' + '. + [(.[] | select(.Name == "immich_s6_net") | .Name = "immich_s6_net_added")]' + 'map(if .Name == "immich_s6_net" then .Driver = "overlay" else . end)' + 'map(if .Name == "immich_s6_net" then .Scope = "swarm" else . end)' + 'map(if .Name == "immich_s6_net" then .Internal = (.Internal | not) else . end)' + 'map(if .Name == "immich_s6_net" then .Attachable = (.Attachable | not) else . end)' + 'map(if .Name == "immich_s6_net" then .Ingress = (.Ingress | not) else . end)' + 'map(if .Name == "immich_s6_net" then .EnableIPv6 = (.EnableIPv6 | not) else . end)' + 'map(if .Name == "immich_s6_net" then .ConfigOnly = (.ConfigOnly | not) else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Driver = "changed" else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Options = {"fixture":"changed"} else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].Subnet = "10.254.0.0/24" else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].Gateway = "10.254.0.1" else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].IPRange = "10.18.128.0/17" else . end)' + 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"fixture":"10.18.0.254"} else . end)' + 'map(if .Name == "immich_s6_net" then .Options = {"com.docker.network.bridge.enable_icc":"false"} else . end)' + ) + + raw=$(/usr/bin/cat "$stage1_root/inventory/docker-networks.json") || return 1 + [ "$(printf '%s' "$raw" | /usr/bin/jq -r '.[] | select(.Name == "immich_s6_net") | .Id')" = "$old_id" ] || return 1 + baseline=$(printf '%s' "$raw" | stage3_docker_network_policy_canonical) || return 1 + printf '%s' "$baseline" | /usr/bin/jq -e 'length == 41 and ([.[].Name] | unique | length) == 41' >/dev/null || return 1 + + changed=$(printf '%s' "$raw" | /usr/bin/jq -c --arg id "$new_id" 'map(if .Name == "immich_s6_net" then .Id = $id else . end)') || return 1 + canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1 + [ "$canonical" = "$baseline" ] || return 1 + + changed=$(printf '%s' "$raw" | /usr/bin/jq -c ' + map(if .Name == "immich_s6_net" then + .Created = "2099-01-01T00:00:00Z" | + .Containers = {"runtime-container":{"Name":"runtime","EndpointID":"runtime-endpoint"}} | + .Peers = [{"Name":"runtime-peer"}] | + .Status = {"runtime":"changed"} + else . end)') || return 1 + canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1 + [ "$canonical" = "$baseline" ] || return 1 + + changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'reverse | map(if .Name == "immich_s6_net" then .IPAM.Config |= reverse else . end)') || return 1 + canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1 + [ "$canonical" = "$baseline" ] || return 1 + + changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"z":"10.18.0.253","a":"10.18.0.254"} else . end)') || return 1 + alternate=$(printf '%s' "$raw" | /usr/bin/jq -c 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"a":"10.18.0.254","z":"10.18.0.253"} else . end)') || return 1 + canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1 + alternate_canonical=$(printf '%s' "$alternate" | stage3_docker_network_policy_canonical) || return 1 + [ "$canonical" = "$alternate_canonical" ] || return 1 + + for filter in "${policy_mutations[@]}"; do + changed=$(printf '%s' "$raw" | /usr/bin/jq -c "$filter") || return 1 + canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1 + [ "$canonical" != "$baseline" ] || return 1 + done + + changed=$(printf '%s' "$raw" | /usr/bin/jq -c '.[1].Name = .[0].Name') || return 1 + ! printf '%s' "$changed" | stage3_docker_network_policy_canonical >/dev/null 2>&1 || return 1 + changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'del(.[0].EnableIPv6)') || return 1 + ! printf '%s' "$changed" | stage3_docker_network_policy_canonical >/dev/null 2>&1 +} + case_veth_drift() { stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 11 aa:bb:cc:dd:ee:ff || return 1 ! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 12 11 aa:bb:cc:dd:ee:ff || return 1 @@ -623,6 +685,7 @@ expect_success 'transient unit collision and wrong ownership token rejected' cas expect_success 'inert signals are deferred through ownership capture and PID start-time identity is exact' case_inert_signal_deferral_and_pid_identity expect_success 'inert write probe cleaned exactly and symlink preserved' case_inert_probe_cleanup expect_success 'actual inert cleanup function removes mocked unit/probe and propagates execution or cleanup failure' case_inert_probe_failure_trap +expect_success 'Docker network policy canonical ignores runtime identity churn and rejects policy drift' case_docker_network_policy_canonical expect_success 'Docker gateway inventory requires exact 39 unique addresses' case_docker_gateway_inventory expect_success 'veth identity drift rejected before cleanup' case_veth_drift expect_success 'table/veth drift aborts each direct cleanup path before deletion' case_direct_cleanup_drift_gates diff --git a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh index f7ca02d..abbde2c 100644 --- a/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh +++ b/docs/le-app-database-migration/phase2b-stage3-namespace-activation/tests/stage3-common.sh @@ -202,8 +202,58 @@ stage3_rootful_docker() { /usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@" } +stage3_docker_network_policy_canonical() { + /usr/bin/jq -eS -c ' + def nullable_string: type == "null" or type == "string"; + def string_map: type == "object" and all(to_entries[]; (.key | type) == "string" and (.value | type) == "string"); + def nullable_string_map: type == "null" or string_map; + def normalized_string_map: if . == null then {} else to_entries | sort_by(.key) | from_entries end; + def ipam_config: + type == "object" and + ((.Subnet // null) | nullable_string) and + ((.Gateway // null) | nullable_string) and + ((.IPRange // null) | nullable_string) and + ((.AuxiliaryAddresses // null) | nullable_string_map); + length as $count | + if type == "array" and $count > 0 and + all(.[]; + type == "object" and + (.Name | type) == "string" and (.Driver | type) == "string" and (.Scope | type) == "string" and + (.Internal | type) == "boolean" and (.Attachable | type) == "boolean" and + (.Ingress | type) == "boolean" and (.EnableIPv6 | type) == "boolean" and + (.ConfigOnly | type) == "boolean" and (.IPAM | type) == "object" and + (.IPAM.Driver | type) == "string" and (.IPAM.Options | nullable_string_map) and + ((.IPAM.Config // []) | type == "array" and all(.[]; ipam_config)) and + ((.Options // {}) | string_map)) and + ([.[].Name] | unique | length) == $count + then map({ + Name, + Driver, + Scope, + Internal, + Attachable, + Ingress, + EnableIPv6, + ConfigOnly, + IPAM: { + Driver: .IPAM.Driver, + Options: (.IPAM.Options | normalized_string_map), + Config: ((.IPAM.Config // []) | + map({ + Subnet: (.Subnet // null), + Gateway: (.Gateway // null), + IPRange: (.IPRange // null), + AuxiliaryAddresses: (.AuxiliaryAddresses | normalized_string_map) + }) | + sort_by([.Subnet // "", .Gateway // "", .IPRange // "", (.AuxiliaryAddresses | tojson)])) + }, + Options: (.Options | normalized_string_map) + }) | sort_by(.Name) + else error("invalid Docker network policy inventory") end' +} + stage3_docker_network_canonical() { - local ids_text raw id + local ids_text raw canonical id local -a ids=() ids_text=$(stage3_rootful_docker network ls --no-trunc -q 2>/dev/null) || return 1 [ -n "$ids_text" ] || return 1 @@ -212,18 +262,14 @@ stage3_docker_network_canonical() { ids+=("$id") done <<< "$ids_text" [ "${#ids[@]}" -gt 0 ] || return 1 - raw=$(stage3_rootful_docker network inspect --format '{"Name":{{json .Name}},"Id":{{json .Id}},"Scope":{{json .Scope}},"Driver":{{json .Driver}},"EnableIPv4":{{json .EnableIPv4}},"EnableIPv6":{{json .EnableIPv6}},"IPAM":{{json .IPAM}},"Internal":{{json .Internal}},"Attachable":{{json .Attachable}},"Ingress":{{json .Ingress}}}' "${ids[@]}" 2>/dev/null) || return 1 - printf '%s\n' "$raw" | /usr/bin/jq -esS -c ' - if length > 0 and all(.[]; - type == "object" and (.Name | type) == "string" and (.Id | type) == "string" and - (.Scope | type) == "string" and (.Driver | type) == "string" and (.IPAM | type) == "object" and - (.EnableIPv4 | type) == "boolean" and (.EnableIPv6 | type) == "boolean" and - (.Internal | type) == "boolean" and (.Attachable | type) == "boolean" and (.Ingress | type) == "boolean") - then sort_by(.Id) else error("invalid Docker network inventory") end' + raw=$(stage3_rootful_docker network inspect --format '{"Name":{{json .Name}},"Scope":{{json .Scope}},"Driver":{{json .Driver}},"EnableIPv6":{{json .EnableIPv6}},"IPAM":{{json .IPAM}},"Internal":{{json .Internal}},"Attachable":{{json .Attachable}},"Ingress":{{json .Ingress}},"ConfigOnly":{{json .ConfigOnly}},"Options":{{json .Options}}}' "${ids[@]}" 2>/dev/null) || return 1 + canonical=$(printf '%s\n' "$raw" | /usr/bin/jq -s -c '.' | stage3_docker_network_policy_canonical) || return 1 + [ "$(printf '%s' "$canonical" | /usr/bin/jq 'length')" -eq "${#ids[@]}" ] || return 1 + printf '%s\n' "$canonical" } stage3_committed_docker_network_canonical() { - /usr/bin/jq -S -c 'map({Name,Id,Scope,Driver,EnableIPv4,EnableIPv6,IPAM,Internal,Attachable,Ingress}) | sort_by(.Id)' "$stage1_root/inventory/docker-networks.json" + stage3_docker_network_policy_canonical < "$stage1_root/inventory/docker-networks.json" } stage3_nonproject_nft_canonical() {