Skip to content

Close in-bar 1.0 security, lifecycle, and operator gaps - #120

Merged
joelst merged 3 commits into
mainfrom
docs/backlog-1.0-gaps
Sep 18, 2026
Merged

joelst merged 3 commits into
mainfrom
docs/backlog-1.0-gaps

Conversation

@joelst

@joelst joelst commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

1.0 was blocked by an inconsistent bar, not a missing feature list. This PR tightens the bar to one local Windows endpoint (macOS evaluation-only; no multi-endpoint manager) and implements the sequenced in-bar waves so the remaining work is process, not unowned code.

Final rubber-duck: do not date or cut 1.0.0 from this PR. Code closes the implementable 1.0 criteria. Ship still needs a signed clean-machine dogfood, recorded Continue/Cline/OpenClaw version pins, a packaged UX walk, and a stable 1.0.0 publish so releases/latest resolves.

What this does

Wave Decision In this PR
0 One local Windows endpoint; drop multi-endpoint from the bar RELEASE_ROADMAP.md, PRODUCT_PLAN.md, BACKLOG.md
1 Audit, don’t invent a security product Pruned ACL, security-boundary suite, exact SDK 1.2.4 pin + warn, CI Foundry-native cache
2 Native tray from app start; quit flush on ExitRequested; streams follow conversation id tray.rs, quit_flush.rs, applyMessagePatch
3 Foundry has no abort — be honest Compare/audio copy; chat Stop already truthful
4 Gateway log is metadata, not bodies onAccess, getHealthRing, Monitor TTFT/tok/s
5 Empty cache blocks; missing tools is not-verified Diagnostics self-test + Flint-verified labels; no fake version pins
6 USER_GUIDE ≠ operator runbook docs/ADMIN.md; Node troubleshooting matches bundled runtime
7 Don’t msiexec on GHA FLINT_RUNTIME_SMOKE=1 / npm run smoke:runtime on Windows CI
8 Install UX now; discovery is the first stable publish About Install / progress / Restart / Later

Still process (not this PR)

  • Signed Windows clean-machine install → load → chat → stop → quit → relaunch
  • Recorded Continue / Cline / OpenClaw version pins (Continue stays verified-unpinned; Cline Unverified; OpenClaw community)
  • Packaged walk of the Wave 6 UX checklist
  • Publish 1.0.0 channel=stable (not a prerelease)

Risks the duck did not paper over

  • Windows CI runtime smoke launches a debug GUI briefly; if WebView2/session flakes, treat as a CI issue, not a reason to add msiexec.
  • Flint-verified labels are this session only (last self-test), not a stored catalog.
  • npm run check still reports pre-existing sidecar/byom-import.js TS errors (untouched here).
  • Local cargo check was not run in this environment (cl.exe missing); CI compiles the new Rust.

Flint seam

  • Frontend pure module — endpoint-self-test.ts, conversation-session.ts (applyMessagePatch)
  • Frontend/SDK boundary — sdk.ts quit/relaunch/flush subscribe, getHealthRing
  • Sidecar command or runtime — getHealthRing, gateway onAccess, runtime pin warn, health ring
  • Rust/Tauri shell — tray, quit-flush handshake, quit_app / relaunch_app, smoke wait-for-ready
  • Packaging/release — capability prune, CI native cache, Windows debug-exe smoke, updater install UX
  • Documentation — 1.0 bar, operator runbook, USER_GUIDE Node row

Hotspots: src/routes/+page.svelte, src/lib/sdk.ts, src/lib/ipc-contracts.ts, sidecar/foundry-sidecar.js.

New IPC command: getHealthRing (query, 10s deadline).

Safety and compatibility

  • Runtime owner remains: Node sidecar under a Rust supervisor (no second manager).
  • New or changed command: getHealthRing; native quit_app, ack_quit_flush, relaunch_app.
  • Effect and cancellation semantics: Stop still does not prove native quiescence. Streams attach to originating conversation id. Compare/audio remain uncancellable and say so.
  • Failure or uncertain-outcome behavior: quit-flush times out at 2s if the renderer is dead. Self-test blocks (does not fail) without a service/model; missing usage / tool_calls is not-verified.
  • Storage, resource, or permission impact: $RESOURCE renderer read scope removed; $RESOURCE write deny kept; opener plugin removed; updater download-and-install kept for Wave 8.
  • 0.7.0 impact: optional follow-on — 0.7.0 stays the evaluation prerelease; this is post-0.7.0 1.0 work.

Validation

  • npm run check — 8 pre-existing byom-import.js diagnostics; no new errors in files this PR owns
  • Focused tests + npm run test:coverage (1130 tests, lines 97.06%)
  • npm run verify:ipc-contracts (36 commands)
  • npm run verify:markdown-links (151 links)
  • npm run verify:bundle — not re-run here (resource map unchanged; sidecar/SDK still packaged as before)
  • git diff --check (README trailing spaces from this work stripped)

Handoff

  • Lane owner: 1.0 bar / release operator (this stack crosses persistence, IPC, sidecar, Rust, packaging, docs — do not split mid-wave without re-checking IPC contracts).
  • Next contributor should know:
    • Do not add embeddings, scheduler, or Azure to close 1.0.
    • Do not mark Cline/OpenClaw verified without a recorded run and testedWith.
    • First stable 1.0.0 is the updater acceptance test (releases/latest/download/latest.json).
    • Clean-machine dogfood is the remaining ship gate; smoke:runtime is not a substitute for loading a model.

Align the 1.0 bar to one local Windows endpoint, then implement the sequenced waves: capability audit, native tray and quit-flush, conversation-scoped streams, observability, endpoint self-test, operator runbook, updater install UX, and Windows debug-exe runtime smoke.
Copilot AI lite review requested due to automatic review settings September 18, 2026 07:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve the native-cache hydration and endpoint self-test correctness and timeout issues before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR hardens the Windows-focused 1.0 release across lifecycle, security, diagnostics, packaging, CI, and documentation.

Changes:

  • Adds native tray, quit-flush, relaunch, and conversation-scoped streaming.
  • Adds endpoint self-tests, health telemetry, runtime warnings, and updater UX.
  • Tightens capabilities, CI caching/smoke tests, and release documentation.
File summaries
File Description
vite.config.js Updates coverage modules.
src/routes/+page.svelte Integrates lifecycle, diagnostics, streaming, and updater UX.
src/lib/security-boundary.test.ts Adds security boundary tests.
src/lib/sdk.ts Adds lifecycle and health APIs.
src/lib/runtime-capability.test.ts Updates capability tests.
src/lib/operation-outcome.ts Classifies health queries.
src/lib/ipc-deadlines.ts Adds query deadlines.
src/lib/ipc-contracts.ts Adds health-ring IPC support.
src/lib/integrations.ts Adds integration metadata.
src/lib/endpoint-self-test.ts Implements endpoint checks.
src/lib/endpoint-self-test.test.ts Tests endpoint checks.
src/lib/conversation-session.ts Adds archived message patching.
src/lib/conversation-session.test.ts Tests message patching.
src-tauri/src/tray.rs Adds native tray behavior.
src-tauri/src/runtime_manager.rs Exposes runtime phase.
src-tauri/src/quit_flush.rs Implements quit flushing.
src-tauri/src/lib.rs Registers lifecycle and smoke commands.
src-tauri/Cargo.toml Removes opener dependency.
src-tauri/Cargo.lock Updates Rust dependencies.
src-tauri/capabilities/default.json Prunes renderer permissions.
sidecar/health-ring.test.ts Tests health history.
sidecar/health-ring.js Adds bounded health history.
sidecar/gateway.test.ts Tests route classification.
sidecar/gateway.js Adds metadata-only access logging.
sidecar/foundry-sidecar.js Integrates health and runtime telemetry.
sidecar/foundry-runtime-pin.test.ts Tests runtime pin warnings.
sidecar/foundry-runtime-pin.js Defines runtime pin warnings.
scripts/smoke-packaged-runtime.cjs Adds packaged runtime smoke tests.
scripts/hydrate-foundry-native.cjs Handles native cache hydration.
RELEASE_ROADMAP.md Narrows the 1.0 release bar.
README.md Updates limitations and documentation.
package.json Pins the SDK and adds scripts.
package-lock.json Updates dependency lock data.
FLINT_DESIGN_SPEC.md Updates SDK pin guidance.
docs/USER_GUIDE.md Updates bundled Node guidance.
docs/RELEASE.md Documents updater rollback.
docs/README.md Indexes operator documentation.
docs/PRODUCT_PLAN.md Defines implementation waves.
docs/LINUX_BUILD_PLAN.md Clarifies Linux deferral.
docs/BACKLOG.md Updates deferred work.
docs/ADMIN.md Adds the operator runbook.
CONTRIBUTING.md Updates planning guidance.
AGENTS.md Updates the documentation map.
.gitignore Ignores native cache contents.
.github/workflows/release.yml Adds release native caching.
.github/workflows/ci.yml Adds CI caching and Windows smoke.
.github/copilot-instructions.md Updates project guidance.
.changeset/wave-5-7-followthrough.md Records endpoint and smoke changes.
.changeset/wave-3-8-progress.md Records lifecycle and observability changes.
.changeset/wave-2-quit-flush-stream.md Records quit and stream changes.
.changeset/wave-2-native-tray.md Records native tray changes.
.changeset/wave-1-security-ci.md Records security and CI changes.
.changeset/backlog-1-0-gaps.md Updates planning scope.
Review details

Suppressed comments (4)

scripts/hydrate-foundry-native.cjs:43

  • The cache restore is placed in the root preinstall hook, but npm ci removes node_modules before running lifecycle scripts. At this point path.dirname(DEST_DIR) (node_modules/foundry-local-sdk) does not exist, so this guard returns and the cached native payload is never restored; the SDK install then still has to download from NuGet. Move hydration to a lifecycle point after the dependency directory is materialized, or restore through a staging path consumed by the SDK install script.
function restore() {
  if (!dirHasFiles(CACHE_DIR)) return;
  if (!fs.existsSync(path.dirname(DEST_DIR))) return;
  copyDir(CACHE_DIR, DEST_DIR);

src/lib/endpoint-self-test.ts:129

  • This chooses the selected alias before the ID returned by /v1/models, so the check can pass even when the gateway cannot route a model ID from its own envelope (and it can test an alias that is not listed at all). Since this self-test is explicitly meant to verify “a returned ID round-trips into chat,” select a returned data[].id here, or block when the models envelope did not provide one.
    src/lib/endpoint-self-test.ts:197
  • hasDelta only detects any JSON object in an SSE data: line, so an empty delta/role-only event plus [DONE] is reported as “delivers a token.” This can mark streaming as verified without receiving content; parse the SSE choices and require a non-empty token delta before passing.
    src/routes/+page.svelte:4966
  • When a stream is no longer visible, every token calls saveConversations(), which synchronously rewrites the whole localStorage archive. Long generations will therefore perform a blocking storage write per token, causing UI/event-loop stalls and unnecessary quota/retry pressure. Batch or debounce archive patches and persist on a bounded cadence/finalization instead.
  • Files reviewed: 50/53 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lib/endpoint-self-test.ts
Comment thread src/lib/endpoint-self-test.ts
Comment thread src/lib/endpoint-self-test.ts Outdated
Co-authored-by: joelst <30506169+joelst@users.noreply.github.com>
Co-authored-by: joelst <30506169+joelst@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 18, 2026 08:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Moderate unresolved issues remain in cache restoration, tray/quit handling, and endpoint self-test correctness and timeouts.

Review details

Suppressed comments (11)

Previously missed (1) — in code that hasn't changed since the last review.

src/lib/endpoint-self-test.ts:254

  • The disconnect check aborts immediately after starting fetch, before waiting for headers or any streamed bytes. That can pass solely because the client cancelled a request before it was dispatched, so it does not exercise gateway disconnect handling or establish anything about native generation cancellation. Start a real streaming response, then abort while its body is in flight (and assert the request reached the endpoint) before reporting this check as passed.

package.json:19

  • This cache restore runs from the root preinstall hook, while npm ci removes/recreates node_modules and the foundry-local-sdk package (and its install script) has not been installed yet. Copying into node_modules/foundry-local-sdk/foundry-local-core at this point can therefore be deleted by dependency installation, so the SDK still downloads from NuGet on cache hits. Restore after the package is materialized, or integrate the cache with the SDK's installer, and add a CI assertion that a cache hit avoids the download.
    "preinstall": "node scripts/hydrate-foundry-native.cjs --restore",

scripts/hydrate-foundry-native.cjs:42

  • npm ci removes node_modules before running the root preinstall lifecycle, so node_modules/foundry-local-sdk (and its parent) does not exist yet. This guard therefore returns on every clean install and the restored cache is never copied before the SDK's install script runs, defeating the CI/release cache. Create the destination parent (or remove this guard) instead of treating its absence as a reason to skip restore.
  if (!fs.existsSync(path.dirname(DEST_DIR))) return;

src-tauri/src/lib.rs:49

  • If native tray creation fails, this only logs the error while the frontend still defaults keepServiceInBackground to true and prevents window close. Since the frontend tray was removed, a tray initialization failure can leave a hidden window with no Open/Quit recovery control. Fail startup or disable close-to-tray when tray::install fails.
    src-tauri/src/tray.rs:34
  • AppHandle::exit is the immediate-exit API, so the native tray Quit path bypasses the ExitRequested event that emits flint-quit-flush. When the renderer is alive, this can terminate without flushing conversations; when it is dead, it is fine to fall back to the timeout path. Request an exit through the event-driven API here (with the existing timeout handling) rather than calling immediate exit directly.
    src/lib/endpoint-self-test.ts:304
  • This branch treats any response containing tool_calls as a pass, even when the gateway returned an HTTP error (for example, a 500 JSON error that happens to include that field). Check res.ok before accepting the response, as the chat check does, so Flint-verified cannot be granted for a failed request.
    src/lib/endpoint-self-test.ts:229
  • hasDelta only checks that an SSE data: payload contains {; it does not verify that any chunk contains a token/content delta. A stream with only role/metadata chunks plus [DONE] will be marked as delivering a token. Parse the SSE data and require a non-empty content delta (or another explicit first-token signal) before passing this check.
    src/lib/endpoint-self-test.ts:187
  • usageSeen is set from any JSON body before checking res.ok or the response shape. A failed HTTP response containing a usage field could therefore pass the usage check even though no completion succeeded. Only record usage after accepting a successful completion response.
    src/lib/endpoint-self-test.ts:159
  • When no alias is selected, this chooses the first cached /v1/models entry, but that endpoint can contain vision/STT models as well as chat models. The self-test then reports chat/stream failures instead of the promised blocked state when no cached chat model is available. Restrict the candidate to the selected chat-capable catalog model (or block when there is none) rather than treating an arbitrary cached model as a chat target.
    src/lib/endpoint-self-test.ts:159
  • When a model is selected, this bypasses the ID returned by /v1/models, so the self-test can pass for a stale/unlisted alias and never verifies the documented “returned ID round-trips into chat” behavior. Select the returned model ID (or explicitly fail when the requested model is not present) before issuing the chat requests.
    src/lib/endpoint-self-test.ts:130
  • These direct fetch calls have no timeout or cancellation budget. If the local endpoint accepts a connection but stalls on headers or a response body, the Diagnostics button remains busy indefinitely and the later checks never run. Use a bounded per-check/overall deadline and abort the active request when it expires.
  • Files reviewed: 51/54 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

joelst commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Follow-on: this stack lands as 0.9.0, the first stable (skip 0.8.0), so we can prove the in-app updater from 0.7.0 evaluation before 1.0.0. Version bump and embeddings path are on #121. Do not tag 1.0.0 as the first releases/latest pointer.

joelst commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the Copilot review on the stack tip (#121, fccb320) because endpoint-self-test.ts also gained embeddings checks there.

Fixed

  • Flint-verified requires a passing /v1/models envelope.
  • Chat/embeddings round-trip a listed model id, not an unlisted UI alias; STT-only lists block chat.
  • Stream requires a content token; usage/tools require res.ok.
  • Fetches are bounded; disconnect waits for an in-flight stream before abort.
  • Foundry native cache: CI uses npm run ci:deps (extract → restore → rebuild). Root preinstall cannot survive npm ci extract-after-preinstall.
  • Native tray install failure now fails startup (close-to-hide has no Open/Quit without it).
  • Background stream archive writes are debounced.

Not changed

  • Tray Quit still calls AppHandle::exit. In Tauri 2.11 that does emit ExitRequested (runtime_handle.request_exit), so the quit-flush handshake still runs. Immediate std::process::exit is only the fallback if request_exit fails.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Smoke failures can be reported as successful, and the native cache restore runs too late to protect dependency installation.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 51/54 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread package.json
Comment thread sidecar/gateway.js
*/
export function classifyGatewayRoute (urlPath) {
const path = String(urlPath || '').split('?')[0];
if (path.includes('/chat/completions')) return 'chat';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. classifyGatewayRoute now matches chat/completions, embeddings, and models as path segments (/(^|\/)chat\/completions(\/|$)/ and the same shape for the others). /v1/chat/completions-evil is other. Landed on #121 (c70b086).

Comment thread src-tauri/src/lib.rs
@joelst
joelst added this pull request to stack #123 September 18, 2026 19:37
@joelst
joelst merged commit ae14cf4 into main Sep 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants