Commit Graph

116 Commits

Author SHA1 Message Date
cc6bcdceaa scroll fixes + activity bar position
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- Activity bar moved to bottom of message list (before messagesEnd)
- Smart scroll: auto-scroll only during streaming or when near bottom
- Scrolling up pauses auto-scroll until next send
- Removed duplicate $effect block
- Plan: tool timeline in sidebar (plans/2026-07-14-tool-timeline-sidebar.md)
2026-07-14 11:37:52 +02:00
5b403141ea fix: VERSION file resolution in Docker build context
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- Dockerfile now copies VERSION into /build/web/VERSION for vite
- vite.config.ts tries ../VERSION (local dev) then ./VERSION (Docker)
2026-07-14 11:13:58 +02:00
7847cdffd6 add version display in UI sidebar + version bump rules
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- VERSION file at repo root (0.3.0)
- vite.config.ts reads VERSION at build time, injects __OIKOS_VERSION__
- App.svelte shows version in sidebar tooltip + subtle text below logo
- AGENTS.md §9: every commit to main MUST bump VERSION
  (patch=bugfix, minor=new features, major=breaking changes)
2026-07-14 11:11:53 +02:00
dce19bd258 fix: PlanProgress template syntax error — multi-statement inline expression
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
2026-07-14 11:04:47 +02:00
60effcb2fe session reliability: reconnect, knowledge loop, retire request_execution
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
Phase 1 — crash recovery: SSE auto-reconnect + backoff, polling gate
during disconnect, connection banner with retry button, empty-response
retry 3x, non-terminal resume on empty response, persistent error cards.

Phase 2/4 — visibility + continuation: custom ExecutionStatus renderer,
approvals extracted on every tool_result (not just done), activity bar
with status/goal, SessionDigest live polling, Continue button.

Phase 3 — cleanup: complete_task auto-cancels orphaned approvals,
deletes assent/destructive window keys, propose_plan marks pending
steps as replaced, plan step seq-order enforcement.

Phase 5 — knowledge loop: list_lxcs state filter (active/destroyed),
SOUL.md unmissable writeback section, propose_plan validation nudge,
complete_task writeback check, upsert_knowledge about array support,
plan generation grouping in frontend, session approval count badge.

Retire request_execution — all mutations now route through run.
Updated SOUL.md, AGENTS.md, CLIENTS.md, skills, and agent system notes.

Migration 020: plan step generation column, audit_log session_id index,
nomos_plan_executions pending-approval index.
2026-07-14 11:03:23 +02:00
bcf2b265c5 Pass apiUrl through OIDC redirect so SPA has it on return
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The apiUrl configured on the Config page was lost when the webview
navigated away to localhost and back. Now it's included in the return
URL as ?desktop=1&apiUrl=...&token=...
2026-07-14 00:18:21 +02:00
2bd7de355b Desktop OIDC: full page nav to localhost, meta redirect back
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
SPA navigates to 127.0.0.1:18901/oidc/start, passing ret URL.
Go opens browser, waits for callback, saves token, returns HTML with
<meta refresh> back to Wails app with ?desktop=1&token=TOKEN.
main.ts extracts token from URL on reload.
2026-07-14 00:13:01 +02:00
8b3fe02a10 Desktop OIDC: non-blocking fetch + poll, don't leave webview
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
SPA fetches /oidc/open (returns session ID immediately), then polls
/oidc/result every 500ms. Go server opens browser in a goroutine.
Webview never leaves the Wails origin. Token is saved to keychain and
returned through the poll response.
2026-07-14 00:09:46 +02:00
d7197c1952 Desktop OIDC: redirect webview to local server, Go opens browser
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The webview navigates to http://127.0.0.1:18901/oidc/open?apiUrl=...
The Go server opens the system browser to Authentik, waits for callback,
exchanges code for token, saves to keychain, then redirects the webview
back with ?desktop=1&token=TOKEN. main.ts extracts the token from URL.
2026-07-14 00:04:49 +02:00
cac5524402 Detect desktop via URL param not config
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
2026-07-14 00:02:12 +02:00
56e509d506 Desktop OIDC: open in system browser via window.open, revert to copy-paste
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The local HTTP server approach (fetch to 127.0.0.1) doesn't work in the
Wails webview. Simplify: use window.open() to launch OIDC in the real
browser. After authentication, the callback page at the server shows the
token. User copies and pastes into the Token tab.

Also fix: SetSize before app.Run() crashes with nil pointer — use
WebviewWindowOptions width/height directly from restored state.
2026-07-14 00:00:24 +02:00
5664a4bf29 OIDC: local HTTP server instead of Wails bindings
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The Wails runtime isn't reliably loading for IPC calls. Replace the
binding-based StartOIDCLogin with a local HTTP server on 127.0.0.1:18901:

- /oidc/login?apiUrl=... — opens system browser, waits for token
- /oidc/callback — Authentik redirect target, exchanges code
- /oidc/config?apiUrl=... — fetches OIDC provider config
- SPA detects desktop via ?desktop=1 URL param
- SPA calls localhost directly via fetch() instead of Wails IPC
2026-07-13 23:52:08 +02:00
ff6608f9ea Use default Wails asset handler + GetStoredConfig binding
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- Remove custom asset handler — it broke Wails IPC routing
- Use application.AssetFileServerFS(distFS) so Wails serves its own runtime
- Add GetStoredConfig binding: SPA calls it on startup to retrieve keychain config
- main.ts: loadDesktopConfig() fetches stored creds before mounting
- Remove runtime.js embed (Wails serves it internally)
2026-07-13 23:45:54 +02:00
0271727709 Embed Wails runtime.js, serve it from custom asset handler
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The SPA needs /wails/runtime.js for window.wails to be available.
Since we use a custom AssetOptions.Handler, Wails' internal routing
doesn't serve it. Embed the runtime and serve it explicitly.
2026-07-13 23:40:50 +02:00
c31978042f Desktop OIDC: open system browser, capture callback on localhost
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
ConfigService.StartOIDCLogin():
- Fetches OIDC config from the API
- Generates PKCE params
- Starts local HTTP server on 127.0.0.1:18901
- Opens system browser to Authentik
- Captures callback directly (no copy-paste)
- Exchanges code for token, saves to keychain
- Returns token to SPA → auto-connects

Config.svelte detects Wails environment and calls the binding.
2026-07-13 23:37:17 +02:00
f6e2079a61 Fix OIDC login in desktop: sync apiUrl before startLogin, add app icon
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- Config.svelte: call setConfig() before startLogin() so fetchConfig
  uses the user-entered server URL
- Makefile: copy icon.icns into .app bundle Resources
- Info.plist: add CFBundleIconFile entry
2026-07-13 23:27:12 +02:00
515c9b9174 fix(web): stack auth options vertically in config screen
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
2026-07-13 23:16:50 +02:00
f1ac82255a Add OIDC desktop callback, app logo, rename to Oikos
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
- Server: /oidc-callback HTML page exchanges Authentik code for token,
  displays it for user to copy into the desktop app's Token tab
- oidc.ts: desktop mode uses apiUrl+/oidc-callback as redirect URI,
  encodes PKCE verifier in state parameter
- Config.svelte: add Server URL field to OIDC tab for desktop UX
- Caddy: add /oidc-callback to enroll bypass (no Authentik gate)
- App: favicon.png as system tray icon, window title 'Oikos'
- web/index.html: title 'Oikos'
2026-07-13 23:14:40 +02:00
62a337f3cc feat(web): redesign config screen with animated particle background and unified auth layout
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
2026-07-13 23:12:16 +02:00
07d67c8446 fix(oidc): strip trailing slash from redirect URI to match Authentik strict mode
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
2026-07-13 22:50:31 +02:00
8b50753746 feat(chat): MCP tool apps — custom inline renderers for 12 tools
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
12 of 33 MCP tools now render as rich inline cards instead of raw JSON:
EntityCard, HealthSummary, LXCList, EntityTable, KnowledgeResults,
BlastRadius, ChangeLog, FleetSnapshot, MetricChart.

Architecture:
- Server: annotateJSONResult() wraps queryRows with __renderer hints
- Registry: match/dispatch system maps tool names to Svelte components
- Chat: inline dispatch with 5-card limit, overflow to collapsed group
- ToolCallGroup: unmatched prop, hides when all matched, ARIA labels

Tests: 3 new Go tests for annotateJSONResult (wrap, no-op, multi-row).
2026-07-13 22:46:56 +02:00
04006553a3 Wails v3 desktop app: scaffold, shell features, token mgmt, auto-update, CI
Problem: the Oikos control room was browser-only — no native desktop
experience (system tray, notifications, keychain-persisted auth).

Change: add a Wails v3 thin-shell desktop app at cmd/desktop/ that embeds
the existing SPA in a webview. The Go side is ~380 lines — no bundled
server, no Postgres connection. It reads auth from the OS keychain,
injects it into the SPA on load, and the SPA talks HTTPS to the homelab
same as a browser.

Phase 1.0 — Scaffold + window:
  - Embed web/dist/ into the Wails binary
  - Inject window.__OIKOS_CONFIG__ with keychain-stored apiUrl + token
  - 1400×900 window, min 1024×700
  - System tray: Open/Quit, click toggles window

Phase 1.1 — Native shell:
  - Poll /api/v1/dashboard/summary every 30s; osascript notification
    when approvals or critical signals increase
  - Save/restore window position to ~/.config/oikos/window.json
  - EnableAutoStart/DisableAutoStart — macOS LaunchAgent plist

Phase 1.2 — Token management:
  - Config.svelte calls window.wails.Call.ByName('SaveConfig') after
    successful connection — persists to OS keychain
  - ConfigService binds SaveConfig, ClearConfig, EnableAutoStart,
    DisableAutoStart to the Wails runtime

Phase 1.3 — Auto-update:
  - Poll Gitea releases API every 6h, compare semver, show dialog
  - 'Check for Updates' tray menu item triggers immediate poll

Phase 1.4 — Distribution:
  - macOS entitlements.plist: network client + keychain access
  - .gitea/workflows/desktop.yml: CI builds macOS arm64 + Linux amd64
    on 'desktop-*' / 'v*' tags, attaches artifacts to release
  - Makefile: desktop (build), desktop-package (build + zip/tar.gz)
  - CONTRIBUTING.md: documented desktop app + commands

Risk: low. Wails v3 alpha API may shift; the Go glue is ~380 lines and
trivially portable. The desktop app is additive — zero changes to the
existing server or SPA logic. No config mutation, no infrastructure
impact.

Verification: go build, go vet, go mod tidy all pass.
2026-07-13 22:40:18 +02:00
f6a699469d oidc: authenticate SPA users via Authentik
- Add OIDC proxy endpoints (GET config, POST token) to API server
- Implement PKCE Authorization Code flow in SPA
- Enable Authentik login tab in Config page
- Handle callback + auto-refresh + session restore
- Add restart: unless-stopped to all persistent services
- Configure OIDC issuer + client_id in docker-compose
2026-07-13 22:17:31 +02:00
4c4afc4783 fix(web): fit graph to node bounding box once the simulation settles
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Fresh nodes (no prior x/y) get placed by d3-force's default init, which
spirals out from the ORIGIN — not (width/2, height/2) — while the
centering forces here are deliberately weak (0.04, so they don't fight
the link/collide layout) and alphaDecay stops the sim before a weak force
can always pull a far-off cluster back to center. Net effect: graphs could
settle visibly off-center on load, cramped in a corner of the pane.

Fixed by computing the actual node bounding box once the simulation's
'end' event fires and setting the view transform to fit it, instead of
relying on the force balance to land on center by itself. Gated behind a
`fit` flag so passive background reloads (live entity/relationship
events) don't yank the view out from under someone actively panning or
zoomed in on a specific area — only fresh loads (mount, root/depth
change, reset, re-root) reframe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 10:51:56 +02:00
335fa67d55 fix(web): scope 1-hop neighbor expansion to rooted graph views only
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Two bugs found while verifying against real production data:

- Excluded activity types (execution/check/task etc., see categories.ts)
  were falling through inCategory's "unknown type -> always visible"
  fallback, since typeCategory only stored entries whose category was
  defined. That fallback exists for types the ontology never returned at
  all; it wrongly re-admitted types the ontology returned but categories.ts
  deliberately excludes. Fixed by storing every type (including undefined
  categories) and checking key presence, not value truthiness.

- Once that was fixed, the previous commit's 1-hop neighbor expansion
  (dimmed cross-category context) turned out fine for a rooted view but
  flooded an unrooted "browse the whole category" view: Fleet's ~49 focus
  entities are hub-like enough that 1-hop pulled in 325+ of the system's
  479 total entities. Neighbor expansion now only applies when a root is
  set; the unscoped view goes back to same-category-only edges, which
  measured at a clean 49 nodes for Fleet.

Verified against live production data (real bearer token, real DB) rather
than mocks: Fleet unrooted = 49 nodes matching the DB's compute+physical
count exactly; rooting on host:strong shows 33 nodes with both bright
same-category and dimmed cross-category neighbors, no isolated dots.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 10:39:31 +02:00
61ad785fef fix(web): render full document content, keep graph connected under categories
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Two fixes to the new category taxonomy:

- Knowledge Base couldn't show a document/investigation/runbook's own
  markdown body — knowledge_entities.content was never exposed by any
  endpoint (GetEntityKnowledge answers "what knowledge references this
  entity", not "what is this entity's content"). Add GET
  /api/v1/knowledge/content/{id} and render it with the existing
  marked+DOMPurify pipeline in a new Content section.

- The graph hid any edge whose other endpoint wasn't in the active
  category, so nodes with only cross-category neighbors rendered as
  disconnected dots. Queried the real relationship table: ~70% of infra
  edges cross Fleet/Network/Services/Storage lines (compute+network+
  software+storage+physical used to be one "infrastructure" layer).
  EntityGraph now keeps 1-hop neighbors visible but dimmed instead of
  hiding them, so the edges — and what they connect to — stay visible.

- categories.ts: `cognition` domain conflated true knowledge (document/
  investigation/runbook, 58 entities) with operational telemetry
  (execution/check/task/signal/approval/pattern/skill/classification/
  feedback, 300+ entities with their own Operations/Signals/Learning
  pages). Mapping the whole domain to Knowledge pulled in 245 execution
  entities fanning out from ~17 compute nodes via `targets` edges — the
  single biggest source of graph clutter. Knowledge now maps by type
  (document/investigation/runbook only); the rest of cognition is
  excluded from Knowledge Base browsing entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 10:17:13 +02:00
35c54ceef5 feat(web): browse Knowledge Base by mixed Network/Fleet/Services/Storage/Identity/Knowledge categories
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Replace the layer-based (Infrastructure/Governance/Cognition) browsing tabs
with a synthesized category taxonomy built from the ontology's finer-grained
`domain` field, since layer lumped unrelated entity types (an LXC and a DNS
record and a storage volume) into one bucket. Network and Fleet each span
two domains, so the table view now fans out per-domain fetches and merges,
while the graph view maps domain->category client-side. Also carries over
several detail-panel polish items (Tasks-not-raw-executions, slug URL
encoding, MultiSelectFilter) from earlier in this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 09:53:15 +02:00
94c94c0758 feat(web): merge Entities + Graph into a single Knowledge Base page
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Replaces the separate Entities/Graph nav items with one Knowledge Base
page that browses all entities as either a table or a force-graph,
scoped by ontology layer (Infrastructure/Governance/Cognition), with a
resizable browse/detail split instead of a slide-over sheet.

- New KnowledgeBase.svelte: layer tabs, view toggle, resizable
  browse/detail split (pattern from Chat.svelte's rail).
- EntityTable/EntityGraph extracted as presentational sub-components;
  their search/filter/root/depth toolbars live in the shared page
  toolbar (not the resizable pane) so they don't truncate when the
  divider is dragged narrow, and both views start flush with the
  detail pane for consistent height.
- EntityTable columns are sortable (slug/type/name/state/health).
- EntityDetailContent redesigned as a single-column list of
  collapsible sections (DetailSection.svelte), collapsed by default
  when empty; relation entries are clickable and select the entity in
  the browse pane + detail pane (and drill in-place in EntitySheet
  wherever it's used elsewhere in the app).
- api.ts: add layer filter to fetchEntities, add fetchEntityTypes for
  client-side graph layer scoping (the graph endpoint has no layer
  param).

Old hash routes (#/entities, #/graph) redirect to #/kb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-12 22:21:29 +02:00
0c0f35a3a9 feat(web): split SPA from oikos binary, require auth on every route
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Phase 0 of plans/2026-07-12-wails-desktop-app.md. The control-room SPA
is no longer embedded (web/embed.go deleted); it's a standalone static
build served separately (make ui / make deploy-ui). The api process
adds CORS and drops the dev-open auth bypass — every route now needs a
real bearer token, including SSE (?token= query param, EventSource
can't set headers) and api's own /agent proxy to nomos (previously
unauthenticated by omission).

nomos was an unauthenticated client of api's /mcp and approval-decision
endpoints; closing dev-open would have broken it, so it now sends
Authorization: Bearer $OIKOS_MCP_BEARER_TOKEN on every call back to api.

SPA gets a runtime config module (config.ts) and a Config.svelte
first-launch/reconfigure page, reachable afterwards via a "Connection"
entry in the sidebar footer. Every fetch() in api.ts routes through
fetchWithAuth so the same build works same-origin (browser prod, Vite
dev proxy) or cross-origin (future Wails webview, remote access).

Six gaps found against the plan and the live Caddy topology while
implementing — documented in the plan's "Plan review" section, most
notably: api's own /agent mount was never behind combinedAuth (fixed),
and production's Authentik forward-auth needs a bearer-token bypass for
API routes that this repo's Caddyfile.oikos reference copy now has, but
the real dtoro/caddy-conf deploy does not yet.

Verified live: cross-origin static SPA + API, CORS, bearer auth, SSE
query-token auth, and localStorage persistence all confirmed working
in-browser. Full Go test suite and npm run build pass with no
regressions against the pre-change baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 15:49:42 +02:00
6807e353e3 feat(web): redesign Overview as the homepage with a living graph backdrop
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Overview replaces Tasks as the default route: a centered new-task entry
with live fleet metrics, a scrollable/filterable task table, and an
ambient canvas rendering of the real entity graph (autonomous camera
drift + mouse parallax) behind it. Tasks sidebar entry is removed;
its status-bucketing logic moves to lib/tasks.ts for reuse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 11:07:36 +02:00
0ed171507f Merge remote-tracking branch 'origin/main'
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
2026-07-12 09:18:23 +02:00
e3cbaee534 fix(knowledge): populate hit id in search results
Search hits and entity-knowledge hits never selected an id column, so
every KnowledgeHit.Id defaulted to the zero UUID. The frontend's keyed
{#each results as hit (hit.id)} then had all-duplicate keys, which
silently broke Svelte 5's if-block branch swap for the results panel —
search would set searched=true (Clear button appeared) but the view
never switched away from "Recently learned". Select e.id in both
queries and key the each block on hit.slug (guaranteed unique) instead.
2026-07-12 09:16:03 +02:00
de126daf43 feat(web): fold Events/Agent/Audit into EntityDetail; tag agent_activity with entity_id
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Events, Agent, and Audit were standalone read-only pages that never
cross-referenced the entity they related to. Fold them into EntityDetail
as entity-scoped cards (Agent activity, Audit trail) alongside the
existing Signals/Executions/Knowledge cards, and give the Signals card
real Ack/Mute/Resolve actions. Signals stays a standalone page since
it's the only one with cross-entity triage value (badge count, actions).

Also fixes the underlying reason those new cards would've stayed empty:
agent_activity rows were never tagged with entity_id at insert time
(cmd/nomos/store.go, internal/mcp/server.go), even though the column
and the API filter both support it. Added a best-effort resolver that
checks common tool-arg keys (target, entity_slug, slug, ...) against
the entities table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 08:38:19 +02:00
fb4c76ba82 fix(ui): implement UI review findings — a11y, IA, and consistency fixes
Fixes the reviewed gaps: keyboard-inaccessible delete controls (SessionRail,
Entities row), case-sensitive entity filter, two competing entity-detail
navigation patterns (standardize on EntitySheet), non-clickable Overview KPI
cards, a bare button bypassing the shared Button component, inconsistent
blur-only vs live filtering, and an unenforced sanitization assumption on
search snippet HTML (now using the already-present dompurify dependency).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 21:52:59 +02:00
6a8fb435ad fix(concurrency): per-session stream controllers, not one global slot
Closes the known gap flagged in the previous commit (9131559). A single
module-level `activeController` meant cancelStream()/newChat() always
aborted whichever stream was MOST RECENTLY STARTED, regardless of what the
operator was currently viewing: start Task A, switch to an already-loaded
Task B, click "New task" — the click's cancelStream() would silently abort
Task A's still-running turn, even though the operator was never looking at
it and never asked to cancel it.

- Replaced the single controller with activeControllers (Map<sessionID,
  AbortController>) plus pendingController for the brief pre-'session'-event
  window of a brand-new task. Registered immediately in sendMessage (keyed by
  the continuing session id right away, or held pending until the 'session'
  event assigns a new one) and cleaned up on completion.
- cancelStream() now looks up by $currentSession (falling back to
  pendingController when no session is assigned yet) — it can only ever
  touch the stream belonging to the view being left, never an unrelated
  background task's.
- newChat() unchanged in behavior (still calls cancelStream()), now correctly
  scoped through the above.

Verified live, reproducing the exact bug: started Task A (slow, 5 tool
calls), switched to an existing Task B, clicked "New task" while viewing
B — Task A was NOT aborted, ran to completion server-side with a full,
correct final summary (previously this exact sequence would have killed it).
Confirmed the positive path is unaffected: started a task, clicked Stop
while actively viewing it — input re-enabled, stream genuinely aborted
("BodyStreamBuffer was aborted"), turn stopped mid-flight as expected.

This closes out Fix 2's scope from
plans/2026-07-11-concurrent-task-execution.md; only Fix 3 (per-session MCP
client pool, throughput) and the optional Fix 4 (concurrency cap) remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:02:50 +02:00
9131559ebd fix(concurrency): guard chat.ts's stream callback against a stale session
Fix 2 of plans/2026-07-11-concurrent-task-execution.md. sendMessage's SSE
callback mutated the global messages/currentSession stores unconditionally,
assuming only one task's turn is ever in flight. It isn't — the backend runs
every /chat request as its own goroutine with no serialization. Switching to
a different task while a previous one was still streaming let that
background stream's later events (tool_use, text_delta, ..., and worst of
all 'done''s currentSession.set) get applied to whatever the operator is now
looking at: corrupting another task's transcript, or yanking the view back
to the one they left.

- Captures the session a stream belongs to (openedFor at call time, updated
  to the real id once the 'session' event assigns one) and checks
  $currentSession still matches before every messages/error/streaming
  mutation. The task keeps running server-side regardless — dropped events
  just mean the live view isn't watching it; navigating back re-hydrates via
  REST, same as already happens for auto-continuation.
- The 'session' event itself only claims currentSession if the operator
  hasn't already navigated elsewhere since the call started (comparing
  against openedFor, which is null for a brand-new task).
- loadSessionMessages/newChat now reset `streaming` to false unconditionally
  on navigation — needed so the new guard can't leave a DIFFERENT task's view
  stuck showing streaming=true (which would also silently stop startPolling's
  loop from ever applying updates, since it bails while $streaming is true).

Known residual gap, not fixed here (matches the plan's "contained fix, not a
rearchitecture" scope): activeController is still a single global slot, so
starting a new task while another is mid-stream, then clicking "New task"
again, aborts whichever stream that slot last pointed at rather than only
the one being left. A genuine multi-session controller/store is the
plan's deferred "stretch" fix, not required for correctness here.

Verified live: started Task A with a deliberately slow 4-tool-call turn,
switched to an existing Task B mid-stream — Task B's transcript stayed
correct with zero A-originated entries and the input was NOT stuck disabled.
Task A kept running and completed normally server-side (status=done, full
6-tool transcript, 5-entity graph); navigating back loaded its complete,
uncorrupted result via REST.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:34:02 +02:00
5384499903 fix(tasks): plan panel showed only the latest step, not the full plan
Root cause: proposePlan unconditionally deleted and replaced the whole
session_plan_steps list on every call. The model isn't strictly held to
"call propose_plan once with the full list" — nothing stopped it (and
production evidence + live testing showed it happening) from calling
propose_plan once per step as it worked. Each such call wiped every
already-completed step, so the operator only ever saw the model's latest
single step ("1/1") instead of the real, growing plan.

Fix, two layers:
- store.go: proposePlan now only does a destructive replace when no step
  has left 'pending' yet (a genuine pre-execution revision). Once any step
  has started, a new call APPENDS after the current max seq instead of
  wiping — so the panel accumulates the full history regardless of how the
  model chooses to call the tool. plan.proposed now carries `appended` so
  the frontend knows whether to replace or append.
- workspace.ts: plan.proposed handler respects `appended` (update vs set).
- tasks.go / SOUL.md: strengthened the propose_plan description and task-
  loop guidance to call it ONCE with the complete step list end-to-end,
  using update_plan_step (not re-calling propose_plan) to advance — fixing
  the root behavioral cause, with the store-side append as a safety net
  that holds even if the model still calls it incrementally.

Verified: forced the exact incremental-call pattern (propose_plan with 1
step, mark it running, propose_plan again with 1 more step) — the second
call appended at seq 2 instead of erasing seq 1, and its plan.proposed
event carried appended=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:07:10 +02:00
991e7d0900 feat(tasks): phase 6 — live TaskContextPanel (goal, plan, question, entities)
Replaces the chat right rail's ad-hoc Digest+Graph stack with a single
TaskContextPanel that renders the task's live working state, driven by the
always-on events stream (not the per-turn chat SSE) so it keeps updating
during server-side auto-continuation/resume:

- GoalHeader: goal + status pill (planning/executing/awaiting_input/done/
  failed), sourced from the sessions list.
- PlanProgress: ordered steps with live status icons + progress bar, hydrated
  via new GET /sessions/{id}/plan; clicking a step with a target opens its
  EntitySheet (no fake "jump to transcript" — bits-ui Collapsible content
  isn't force-mounted, so a DOM-scroll jump would silently no-op for
  collapsed tool groups).
- OperatorQuestion: the pinned structured question card (prompt/why/entity
  chips/option buttons/free-text), hydrated via new GET /sessions/{id}/
  questions; answering POSTs to the existing answer endpoint.
- SessionGraph upgraded to a live entity panel: entity.touched pulses the
  node (animated ring) and shows "Now touching <slug>"; health.changed shows
  a transient diff badge for touched entities.
- SessionDigest gains a success/failure/partial outcome banner and now also
  refetches when the task's status changes, not just on session switch.

Two bugs found and fixed while wiring this up:
- workspace.ts's status-refresh trigger only covered goal.set/task.status;
  question.raised/answered didn't refresh the sessions list, so GoalHeader's
  pill went stale after answering via the panel (resumeSession runs entirely
  server-side — no client 'done' event to piggyback a refresh on). Now every
  status-affecting event triggers the (debounced) refetch.
- Forgot to rebuild the nomos container after adding the /plan and
  /questions endpoints, so they silently fell through to the old default GET
  handler — caught via a live curl diff against the running container,
  not a code read.

Verified end-to-end against the live stack: goal/plan/question all update
without a reload as the agent works; answering a question via the panel
resumes the agent and the header pill correctly flips to Executing;
entity.touched pulses the live graph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 13:52:36 +02:00
413bf54daf feat(tasks): task board UI — chat window becomes Tasks + card grid
Reframes the chat surface as tasks:
- New Tasks.svelte: a card grid of tasks, each showing status (Running /
  Needs input / Done / Failed), the goal as title, the outcome summary, and
  relative time; filterable by status with live counts; delete on hover;
  "New task" and per-card click open the conversation.
- Board updates LIVE off the events stream (goal.set / task.status /
  question.*) via an explicit liveEvents.subscribe with a debounced refetch —
  scanning all events newer than the last seen, since entity.touched bursts
  bury task events below index 0.
- App shell: primary nav "Chat" → "Tasks" (board is now the home route),
  "New chat" → "New task", conversation header gets a Tasks / Conversation
  breadcrumb. Removed the superseded Sessions page.
- api.ts Session type carries the task fields (goal/status/outcome/summary).

Also fixes a pre-existing SSE bug that blocked ALL live updates app-wide:
writeSSE emitted `event: <type>`, which EventSource only delivers to
addEventListener(type) handlers — but stores/events.ts (and every page reading
liveEvents) consumes via onmessage, which never fires for named events. So the
live stream delivered nothing to the UI. Dropped the event-name line; the type
is already in the JSON payload, and new event types now need zero client
changes. SSE test still green (it parses data: lines).

Verified in the browser against the live stack: the board renders 50 tasks
with correct status buckets; a goal-driven task appears and flips to a Done
card with its summary in real time without a reload; Events page confirms the
stream now delivers to onmessage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 13:22:27 +02:00
52e16e04ca feat: Learning page — capability timeline + trend, built on real data
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
The plan's "learning view" (runbook success-rate trends, promoted
skills, capability timeline) assumes the patterns/skills/feedback
pipeline is populated. It isn't: all three tables are empty in
production and nothing in the codebase ever writes to feedback, so
building the UI against them today would ship a permanently-empty
page. Scoped instead around data that's real and growing —
executions — while still wiring up /patterns and /skills so the page
needs no rework once that pipeline exists.

New /api/v1/learning/timeline (per-verb first-success date + success
rate, parsed via the existing splitAction helper) and
/api/v1/learning/trend (30-day daily success/fail counts), both
read-only queries against executions. Patterns and skills sections
call the existing (untouched) ListPatterns/ListSkills endpoints and
render an explanatory empty state instead of nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 21:28:19 +02:00
682326382e feat: surface blast radius on approval cards
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Pending-approval cards showed target and risk but not what else the
action would affect — the operator approved config_mutation/destructive
commands blind to downstream impact, even though the graph-walk
(blast_radius() SQL, GetBlastRadius endpoint) already existed and was
just never wired into the approval path.

Fetch it once per pending approval and render "Affects N downstream: …"
on both the normal and destructive approval cards, reusing the existing
fetchBlastRadius() API client function which was already written but
unused anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:52:05 +02:00
ac48390796 feat: global activity feed + session digest
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Ops "Executions" tab showed raw target UUIDs, alphabetical (not
recency) order, and a stale status vocabulary from an earlier schema
iteration — never actually usable as a live "what's happening" view.
Replaced with a new recency-ordered /api/v1/activity/recent endpoint
and matching table (human-readable action summaries, risk/status
badges, duration, inline error preview).

Also added /api/v1/activity/session/{id} + a collapsible SessionDigest
panel in the chat rail, answering "what did this session actually do"
(executions by status, entities touched, knowledge written) — the
missing piece for proactive outcome reporting to be visible in the UI,
not just in the chat transcript.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:09:09 +02:00
ec41c0b828 feat: learning view — make the growing knowledge base visible
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
First slice of the observability/learning UI (the "see the system come alive
and learn" ask). The Knowledge page was search-only — blank until you typed —
so the knowledge Nomos now writes via upsert_knowledge was invisible unless
you knew to search for it. Now the page LEADS with what the system knows and
is learning:

- internal/httpapi/knowledge.go: GET /api/v1/knowledge/recent — recency-ordered
  knowledge + a stats header (total, agent-authored, learned-this-week,
  by-kind). Custom route (not OpenAPI-generated), same auth as the rest.
- web Knowledge page rewrite: stat cards up top (Total / Written by Nomos /
  Learned this week / runbooks-investigations), then a "Recently learned" feed
  with agent-authored notes highlighted and badged "learned by Nomos", tags,
  and relative timestamps. A toggle filters to Nomos-only. Search still works,
  now as a mode you enter/clear rather than the whole page.

This turns "the system is getting smarter" from a claim into something you
watch fill up: every gotcha the agent records shows here within seconds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 19:22:35 +02:00
60edff2065 feat: knowledge write-back (upsert_knowledge) + proactive outcome reporting
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
From the last (successful) TypeType deploy session, two gaps the operator hit:

1. Knowledge write-back — the missing half of the loop.
   The agent could read the knowledge base (search_knowledge/get_entity_knowledge)
   but had no way to WRITE it, so everything it learned (the Dragonfly memlock
   rlimit gotcha, the NAT-hairpin DNS issue, etc.) lived only in an ephemeral
   chat message and was lost — the system could never actually "get better."
   This is the `upsert_knowledge` MCP tool the 2026-07-08 gaps plan called for.
   - internal/mcp/server.go: upsert_knowledge(title, content, about?, tags?,
     kind?) writes a document/investigation/runbook entity + knowledge_entities
     row (search column is generated), upserts by slug so re-titling updates in
     place, and optionally links it to the entity it's about so
     get_entity_knowledge surfaces it there.
   - SOUL.md: capture non-obvious findings/deploys/gotchas as part of finishing
     work, not only when asked "what did we learn".

2. "I had to ask for status multiple times."
   The clearest cause: a long working turn (64 tool calls) that exhausted the
   iteration cap ended with a bare "max iterations reached without final
   answer" — a dead end that forced the operator to ask what happened.
   - cmd/nomos/agent.go: on exhaustion, make one final no-tools LLM call
     (finalSummary) asking for a status report — what was accomplished, current
     state, what remains — so the turn always ends with a real outcome.
   - maxIterations 25 -> 40 (the decomposed per-step pct_create flow legitimately
     needs more steps).
   - SOUL.md: always end a turn with a clear outcome; never end silently or on a
     bare tool call — the operator can't see the tools working and reads silence
     as "nothing happened".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 19:07:11 +02:00
233b5e4519 feat: live visibility into what the agent is running (no more silent waiting)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Operator: "I'd like to be able to see in the chat what the agent is actually
running, right now I just wait while nothing happens." Two compounding gaps:

1. The auto-continuation worker (cmd/nomos/continue.go) had zero live push —
   its result only appeared on a manual page reload, so approving a plan and
   watching the chat looked completely dead even while the agent was actively
   working.
2. Even with polling, continueSession only persisted ONE message at the very
   end of a continuation — a continuation that runs several tool calls before
   concluding would still show total silence for however long that took.

Fixed both:
- web/src/lib/stores/chat.ts: polls the current session's messages every 3s
  between turns (never while a live stream owns the message list) and merges
  in anything new. Started after a live turn ends and when a session loads;
  stopped on new-chat/session-switch.
- cmd/nomos/store.go: insertMessageReturningID/updateMessage — lets a message
  be created as a placeholder and updated in place.
- cmd/nomos/continue.go: continueSession now inserts a placeholder the
  instant it starts (renders as the existing "thinking" dots — immediate
  feedback that something is happening) and updates that SAME row after
  EVERY tool call, not just at the end. A poll within ~3s of any tool call
  landing shows it — individual `run` commands appear as the agent issues
  them, not just the final rolled-up summary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:20:10 +02:00
8950bada44 fix: sshExec had no timeout — a hung remote command blocked forever
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Root cause of "running for 10+ minutes without stopping": a real production
execution (TypeType pct_create) was found genuinely stuck 17+ minutes into a
single blocking SSH call. The container's post_install script was looping on
`getent hosts deb.debian.org`, waiting on a network that could never come up
— the operator's static IP config used gw:192.168.8.1, but the actual gateway
on that subnet is 192.168.8.2, so every network call hung instead of failing
fast (packets dropped, not rejected).

Two compounding bugs made this unrecoverable without manual intervention:

1. sshExec (both internal/httpapi/phase3.go and internal/mcp/server.go) had
   NO execution timeout — `session.CombinedOutput()` blocks until the remote
   command exits, with no deadline. A hung remote process blocks the Go
   goroutine forever; the execution can never leave 'running', and the
   operator has no way to make it stop. Fixed: both now race the SSH call
   against a 10-minute hard timeout, closing the session/client and
   returning a clear "timed out after 10m0s" error if exceeded. (The
   mcp/server.go copy also still had the original "swallowed non-zero exit"
   bug from before that fix was applied to httpapi's copy only — fixed here
   too.)

2. provisionScript's DNS-wait loop assumed `getent hosts` fails fast on no
   connectivity — it doesn't; a black-holed network can make each call hang
   far past the resolver's nominal timeout, so the documented "~90s" budget
   was never real. Wrapped every attempt in `timeout 3` so the wall-clock
   budget is now actually enforced (~2min worst case), and the failure
   message now suggests checking the net0 gateway.

Also fixes the matching UI-side gap (operator's literal question: "is there
a way to get more details? it has been running for 10+ minutes without
stopping"):

- InlineApproval's track() polling loop had its own ~6min ceiling and simply
  STOPPED polling after that — silently going stale before the backend (now
  correctly capped at 10min) could ever resolve. Raised to a 14min ceiling
  with margin, and added a distinct 'stalled' state if that's ever exceeded
  (explicitly says something's wrong, rather than freezing silently).
- The running-card now shows live elapsed time (ticking, from the
  execution's created_at), the actual command being run, and the execution
  ID — previously just a static "this can take a minute" with zero
  information. Also added command display to the destructive pending-
  approval card for full transparency before confirming.

Verified live end-to-end in a real browser (dev server proxying to
production): queued a real command via chat, approved via the button,
watched the elapsed-time counter tick in real time, and saw it transition to
a completed card with real output once the command finished.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:45:47 +02:00
f936098364 fix: approval UI was never mounted; add typed confirmation for destructive
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Root cause of "chat gave me no further feedback — had to go to Ops": two
compounding bugs, found by reading the actual production session transcript.

1. InlineApproval.svelte — all of last session's live-status/self-heal work —
   was never imported or rendered anywhere. Chat.svelte had its own separate,
   much dumber approval bar (no status tracking, no destructive handling, just
   silently disappears after clicking) that WAS the one users actually saw.
   Deleted the dead bar and its state; InlineApproval now renders per-message.

2. chat.ts's extractApprovals hardcoded `tool.name === 'request_execution'`,
   so any approval raised by the newer `run` tool was invisible — no card, no
   feedback, nothing to self-heal, forcing the operator to the Ops page with
   zero acknowledgement in the conversation. This was the actual proximate
   cause of last night's destroy-135 session. Fixed to match on response
   shape, not tool name, so it doesn't silently break again for the next new
   gated tool.

3. Nomos was telling operators "type something like 'I confirm destroy 135'"
   for destructive actions (SOUL.md) but no backend path ever consumed that
   phrase — chat-assent explicitly (and correctly) excludes destructive from
   loose assent, but I never built the alternative. Added
   isTypedConfirmation() (cmd/nomos/assent.go): stricter than loose assent,
   requires an explicit "confirm" statement, only applies to destructive-
   flagged pending approvals.

4. InlineApproval's completed-state hardcoded "Provisioned successfully" —
   wrong/confusing for a destroy or arbitrary `run` command. Now says
   "Completed on <target>" and shows the actual command output, verified live
   against the real destroy-135 execution.

Verified live in a real browser against the production API/DB (dev server
proxying to :8090): the historical stuck session now retroactively renders
both executions as resolved with correct wording and real output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:01:53 +02:00
d52968876a feat: general gated run primitive + chat-assent approval (Layer 0)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Implements the first slice of plans/2026-07-10-general-gated-execution.md:
Nomos gets one general execution tool instead of only a fixed action enum,
gated by an automatic risk classifier, and approval can be granted by the
operator just replying in chat instead of clicking a button.

- internal/policy/command.go: ClassifyCommand(cmd, declaredRisk) — rule-based
  read-only allowlist + destructive denylist, default-escalate to
  config_mutation for anything else. Classification can only ESCALATE the
  caller's declared risk, never de-escalate it (destructive always wins even
  if declared read_only). Compound commands (&&, ;, |, $()) never qualify for
  the read-only fast path. Full test corpus.
- internal/mcp/server.go: new `run` MCP tool — target (host:/lxc:), command,
  purpose, optional declared_risk. Read-only commands execute immediately;
  everything else queues an approval exactly like pct_create today, executed
  via httpapi's existing executeApprovedAction. Also fixes a real latent bug:
  pct_exec resolved an LXC's host attribute without the "host:" prefix, so it
  could never find the Proxmox host — new resolveExecTarget/resolveRunTarget
  helpers (mcp + httpapi) fix this for both the new `run` action and existing
  actions that route through the same execution path.
- internal/httpapi/phase3.go: "run" case in executeApprovedAction; fixes two
  bugs found while wiring this up — (1) DecideApproval hardcoded risk_class to
  'config_mutation' on every approve, silently corrupting the audit ledger for
  every other risk class; (2) denying/revoking an approval never updated the
  linked execution's status, so it stayed 'pending_approval' forever instead
  of reflecting the decision.
- cmd/nomos/assent.go: deterministic (not LLM-judged) chat-assent detection.
  Scoped to the immediately-preceding assistant turn's pending approvals only
  — an old "yes" can't retroactively approve something new. Destructive-risk
  actions are excluded from loose assent. Approves via the same HTTP decision
  endpoint the UI button calls, so both paths share one audit trail.
- web/.../InlineApproval.svelte: self-healing poll — a pending approval card
  now picks up being decided via ANY path (chat assent, Ops page, Matrix), not
  just its own button. Previously the banner stayed stuck showing
  Approve/Deny even after the action had already run elsewhere.
- nomos/SOUL.md: `run` is now the general capability ("no fixed menu, only a
  risk gate"); documents chat-assent behavior and the destructive exception.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:28:00 +02:00
f248508919 feat: robust provisioning (DNS self-heal) + live execution feedback in chat
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Production session provisioned the container but the service never installed:
apt failed with "Temporary failure resolving deb.debian.org" — a static-IP LXC
whose assigned nameserver couldn't resolve. The operator also got zero feedback:
the approval banner just sat there with no running/complete/failed status.

Backend robustness (provisionScript):
- Wait for real DNS/connectivity inside the container before apt, and self-heal
  /etc/resolv.conf to a public resolver (1.1.1.1/8.8.8.8) if the assigned one
  is dead. `set -e` after the gate so apt/post_install failures surface.
- apt-get update/install with Acquire::Retries=3.

Frontend feedback (InlineApproval):
- After approve, poll GET /executions/{id} and show live phase: submitting →
  provisioning… → provisioned successfully / execution failed (with the error).
- add getExecution() to api.ts.

Agent guidance (SOUL.md):
- omit vmid (auto-assigned), prefer dhcp, docker-compose-plugin is not in Debian
  (use docker.io + get.docker.com), end post_install with a health check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:15:21 +02:00
8ed2b88495 fix: pct_create false-success, VMID collision, and stuck approval banner
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Real production failure when the operator clicked Approve in chat: nothing
provisioned, banner never cleared, execution marked completed.

Three root causes:
- sshExec swallowed non-zero exits when the command produced output, so a
  `pct create` that printed "CT 132 already exists" and failed was reported
  as success and a bogus lxc entity was registered. Now any non-zero exit
  returns an error (with output) so the execution is correctly marked failed.
- The LLM reused VMID 132 (belongs to lxc:rclone; VMIDs are cluster-wide).
  pct_create now checks in-use VMIDs via `pvesh get /cluster/resources` and
  falls back to `pvesh get /cluster/nextid` when the requested id is taken.
- InlineApproval.svelte reset its state on every prop change (done was also
  compared against the wrong string), so the banner never cleared and each
  click re-POSTed /decision. Rewritten to track outcome per executionId,
  clear on success, and block resubmits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:44:51 +02:00