This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / configs / omnigent.nix
7.5 kB 191 lines
1let 2 domain = "omni.lassul.us"; 3 port = 8771; 4 stateDir = "/var/lib/omnigent"; 5in 6{ 7 config, 8 lib, 9 pkgs, 10 self, 11 ... 12}: 13let 14 # Server-side agent catalog entry so a browser session can pick omp without 15 # any per-user config: the generic ACP harness drives `omp acp` (omp speaks 16 # the Agent Client Protocol natively). The command is unqualified on purpose 17 # — it is resolved on the *host* that runs the session, not here, so that host 18 # needs omp on PATH (`covibe host` registers one that way). 19 # 20 # This has to be a single-file spec, NOT a bundle directory with config.yaml: 21 # the bundle parser stringifies nested values under `executor.config`, so 22 # `acp_agent` arrives as "{'name': …}" and every turn dies with 23 # "turn setup failed: executor acp_agent must be a mapping with name and 24 # command" — while the session itself sits there looking idle. The single-file 25 # parser keeps it a mapping. `--agent` accepts either shape (spec 26 # materialize_bundle branches on file-vs-directory). 27 ompAgent = pkgs.writeText "omnigent-agent-omp.yaml" '' 28 name: omp 29 description: Oh My Pi (omp) over ACP — runs on whichever machine hosts the session. 30 prompt: You are omp (Oh My Pi), a concise coding assistant. 31 32 executor: 33 harness: acp 34 acp_agent: 35 name: Oh My Pi 36 command: omp acp 37 # See tools/covibe/flake-module.nix: omp fails session/new outright 38 # when omnigent's MCP relay cannot start, and the relay's 39 # `python -I -m omnigent…` child cannot import omnigent under a 40 # nixpkgs-wrapped interpreter. 41 omnigent_mcp: false 42 43 os_env: 44 type: caller_process 45 cwd: "." 46 sandbox: 47 type: none 48 49 # Without a declared terminal the dashboard refuses to open one ("Terminals 50 # can only be created for agents whose spec declares them"), so a session 51 # started from the browser would have no way to poke at the machine it runs 52 # on. One entry is enough; the panel can open several instances of it. 53 terminals: 54 shell: 55 command: bash 56 os_env: inherit 57 ''; 58 59 # One identity per line. Admins bypass every per-session ACL, which is what 60 # makes sharing manageable at all: a session's creator gets LEVEL_OWNER, and 61 # only owners/managers can grant others read (watch + diffs) or edit (steer). 62 admins = pkgs.writeText "omnigent-admins" '' 63 lass@lassul.us 64 ''; 65in 66{ 67 # omnigent: self-hosted agent meta-harness. The server owns sessions, 68 # transcripts, per-session ACLs (read / edit / manage / owner), the per-file 69 # diff view and the browser UI; the agents themselves run on whichever 70 # machine registers as a *host*, so no coding agent runs on neoprism unless 71 # someone points a host at it. `s covibe` in a project directory is the 72 # client: it runs omp locally and registers the session here. 73 # 74 # Browser auth is the local pocket-id IdP. pocket-id has no declarative 75 # client API, so register the client by hand at https://id.lassul.us: 76 # name: omnigent 77 # callback URL: https://omni.lassul.us/auth/callback 78 # logout URL: https://omni.lassul.us/ 79 # then paste its id + secret into the clan prompt below. 80 81 # Generated cookie-signing secret plus the prompted pocket-id client 82 # id/secret, materialised into one EnvironmentFile. 83 clan.core.vars.generators.omnigent = { 84 files."env" = { }; 85 prompts.oidc_client = { 86 description = '' 87 Register an OIDC client in pocket-id (https://id.lassul.us): 88 name: omnigent 89 callback URL: https://omni.lassul.us/auth/callback 90 Generate a client secret, then paste both lines: 91 OMNIGENT_OIDC_CLIENT_ID=... 92 OMNIGENT_OIDC_CLIENT_SECRET=... 93 ''; 94 type = "multiline"; 95 persist = true; 96 }; 97 runtimeInputs = [ pkgs.openssl ]; 98 script = '' 99 { 100 printf 'OMNIGENT_OIDC_COOKIE_SECRET=%s\n' "$(openssl rand -hex 32)" 101 cat "$prompts"/oidc_client 102 } > "$out/env" 103 ''; 104 }; 105 106 systemd.services.omnigent = { 107 description = "omnigent agent meta-harness (${domain})"; 108 wantedBy = [ "multi-user.target" ]; 109 after = [ 110 "network-online.target" 111 "nginx.service" 112 ]; 113 wants = [ "network-online.target" ]; 114 environment = { 115 HOME = stateDir; 116 OMNIGENT_AUTH_ENABLED = "1"; 117 OMNIGENT_AUTH_PROVIDER = "oidc"; 118 OMNIGENT_OIDC_ISSUER = "https://id.lassul.us"; 119 OMNIGENT_OIDC_REDIRECT_URI = "https://${domain}/auth/callback"; 120 OMNIGENT_OIDC_LOGOUT_REDIRECT_URI = "https://${domain}/"; 121 OMNIGENT_OIDC_ALLOWED_DOMAINS = "lassul.us"; 122 # pocket-id emits `email` *and* `email_verified` in the id_token when the 123 # `email` scope is requested (which omnigent does), but the value of 124 # `email_verified` is the users.email_verified column, whose default comes 125 # from the app-config key `emailsVerified` — shipped as false. omnigent 126 # otherwise hard-rejects the login with "Could not determine user email 127 # from IdP". The gate exists for IdPs where a user can self-assert an 128 # arbitrary address; here users are provisioned by hand in pocket-id 129 # (signups are disabled) and the domain allowlist above still applies, so 130 # the marker carries no information for us. 131 OMNIGENT_OIDC_SKIP_EMAIL_VERIFICATION = "1"; 132 # Default is 8 hours, which expires the `s covibe` session JWT roughly 133 # once a working day and forces a browser round-trip through pocket-id. 134 # A week is the pragmatic setting for a single-operator host behind an 135 # invite-less IdP; the tokens live 0600 in ~/.omnigent/auth_tokens.json, 136 # so shorten this if a laptop ever leaves the house untrusted. 137 OMNIGENT_OIDC_SESSION_TTL_HOURS = "168"; 138 OMNIGENT_ADMIN_LIST_PATH = "${admins}"; 139 # The wheel checks PyPI for updates on every start otherwise. 140 OMNIGENT_NO_UPDATE_CHECK = "1"; 141 # Off by default upstream; be explicit, it phones home otherwise. 142 OMNIGENT_TELEMETRY_ENABLED = "0"; 143 }; 144 serviceConfig = { 145 ExecStart = lib.concatStringsSep " " [ 146 (lib.getExe self.packages.${pkgs.system}.omnigent) 147 "server" 148 "--host 127.0.0.1" 149 "--port ${toString port}" 150 "--no-open" 151 "--database-uri sqlite:///${stateDir}/chat.db" 152 "--artifact-location ${stateDir}/artifacts" 153 "--agent ${ompAgent}" 154 ]; 155 EnvironmentFile = [ config.clan.core.vars.generators.omnigent.files."env".path ]; 156 Restart = "always"; 157 RestartSec = 5; 158 DynamicUser = true; 159 StateDirectory = "omnigent"; 160 WorkingDirectory = stateDir; 161 ProtectSystem = "strict"; 162 ProtectHome = true; 163 PrivateTmp = true; 164 NoNewPrivileges = true; 165 ProtectKernelTunables = true; 166 ProtectControlGroups = true; 167 RestrictAddressFamilies = [ 168 "AF_INET" 169 "AF_INET6" 170 "AF_UNIX" 171 ]; 172 }; 173 }; 174 175 services.nginx.virtualHosts.${domain} = { 176 enableACME = true; 177 forceSSL = true; 178 locations."/" = { 179 proxyPass = "http://127.0.0.1:${toString port}"; 180 # The session event stream is SSE (omnigent sets X-Accel-Buffering: no, 181 # which nginx honours) and the terminal/host/runner tunnels are 182 # WebSockets that idle between turns, so the default 60s read timeout 183 # would sever live sessions. 184 proxyWebsockets = true; 185 extraConfig = '' 186 proxy_read_timeout 1d; 187 proxy_send_timeout 1d; 188 ''; 189 }; 190 }; 191}