Skip to content

Bump anchore/sbom-action from 0.9.0 to 0.24.0 - #4

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/github_actions/anchore/sbom-action-0.24.0
Closed

Bump anchore/sbom-action from 0.9.0 to 0.24.0#4
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/github_actions/anchore/sbom-action-0.24.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jul 12, 2026

Copy link
Copy Markdown
Contributor

Bumps anchore/sbom-action from 0.9.0 to 0.24.0.

Release notes

Sourced from anchore/sbom-action's releases.

v0.24.0

⬆️ Dependencies

v0.23.1

⬆️ Dependencies

v0.23.0

v0.22.2

⬆️ Dependencies

v0.22.1

⬆️ Dependencies

v0.22.0

Changes in v0.22.0

⬆️ Dependencies

v0.21.1

Changes in v0.21.1

... (truncated)

Commits
  • e22c389 chore(deps): update Syft to v1.42.3 (#615)
  • 36a5fde chore: update to node 24 + deps (#614)
  • a0a6512 chore(deps): bump actions/setup-node from 6.2.0 to 6.3.0 (#608)
  • 57aae52 chore(deps): update Syft to v1.42.2 (#607)
  • c29e913 chore(deps): bump fast-xml-parser and other deps (#604)
  • 17ae174 chore(deps/test): move to es modules, node:test, single dist file (#595)
  • 6d473d3 chore(deps): update Syft to v1.42.1 (#599)
  • 60619e7 fix tests and bump fast-xml-parser (#598)
  • e2bd58a chore(deps-dev): bump the dev-dependencies group with 3 updates (#592)
  • d032d7d ci(syft auto update): npm ci, not npm install (#597)
  • Additional commits viewable in compare view

@dependabot dependabot Bot added dependencies Pull requests that update a dependency file github_actions Pull requests that update GitHub Actions code labels Jul 12, 2026
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.9.0 to 0.24.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](anchore/sbom-action@f6c3d0f...e22c389)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot force-pushed the dependabot/github_actions/anchore/sbom-action-0.24.0 branch from fcf27e2 to 2e68c3f Compare July 12, 2026 00:28
@dependabot @github

dependabot Bot commented on behalf of github Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Looks like anchore/sbom-action is up-to-date now, so this is no longer needed.

@dependabot dependabot Bot closed this Jul 12, 2026
@dependabot
dependabot Bot deleted the dependabot/github_actions/anchore/sbom-action-0.24.0 branch July 12, 2026 00:37
dovvnloading added a commit that referenced this pull request Jul 29, 2026
…ings #4, #13) (#178)

The audit listed this bug twice - findings #4 and #13 are the same defect
(confirmed identical by an independent re-read of the current code).

Every popover through the shared .overlay-popover base class (View,
Plugins, Pins, Reasoning, Model) is hosted inside .app-canvas-region, which
is overflow:hidden. .overlay-popover itself declared no max-height and no
overflow-y, so content taller than the available space - most visibly the
View popover's FONT section on a short window - was silently clipped away
with no scrollbar and no indication anything was missing.

Added max-height: calc(100vh - 96px); overflow-y: auto; to the shared base
class. Popovers that already set their own max-height (Reasoning, which
opens upward from the composer and caps at min(60vh, 320px)) are
unaffected - their own rule simply wins the cascade, since this is only a
default for consumers that don't set one.

Live-verified at the app's documented 960x600 floor: the View popover's
FONT section now renders in full with no scrolling needed (max-height
504px vs 485px of real content). At a shorter 960x450 window, where the
content genuinely exceeds the available space, the last color swatch is
confirmed off-screen before scrolling and fully reachable after - a real
scrollbar, not a resize-and-hope fix.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dovvnloading added a commit that referenced this pull request Aug 9, 2026
Wraps api_provider.chat()/chat_stream() in a fallback-chain outer layer:
when a task in FALLBACK_ENABLED_TASKS (task_title, task_web_validate -
"naming/triage" per the ADR's own framing) hits a retryable/unavailable
failure after ADR-006's own same-provider transport retry is exhausted, one
more attempt fires against a different provider chosen by the same
auto-policy catalog stage 18.4 built, excluding the provider that just
failed. Correctness-sensitive tasks (task_chat, task_chart, ...) are
untouched - no fallback fires for them, matching "off by default for
correctness-sensitive tasks" from the ADR's decision #4.

The substitution is never silent: on_fallback threads down the same
additive kwarg chain as model_ref/settings_manager, and backend/agents.py's
dispatch surfaces it as a warning notification naming both the provider
that failed and the one substituted in. chat_stream's own fallback is
additionally guarded so it can only fire before any real text has reached
the caller - mirroring the existing "nothing forwarded yet" invariant the
transport-retry layer already established, so a partially-delivered reply
is never silently replayed against a different model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dovvnloading added a commit that referenced this pull request Aug 9, 2026
…alog (#305)

* ADR-018 stage 18.1: ModelRef dispatch + unified catalog

ModelRef(provider, model_id) replaces "task" as the addressing scheme for
which provider+model a chat call actually hits. graphlink_model_catalog.py
gains ModelRef/ResolvedModel, the resolution chain (resolve_model_ref),
three auto policies over the catalog (choose_auto_model_ref: cheapest-
capable/fastest/best-quality, always capability-filtered), and
unified_catalog() - a pure aggregation of Ollama/llama.cpp scan results
plus each API provider's cached catalog into one list, annotated with
per-mtok cost via a caller-supplied price_lookup (token_counter.py's
pricing table, extracted into price_per_mtok so both the cost estimator
and the catalog price off the same numbers).

api_provider.chat()/chat_stream() accept an optional model_ref kwarg that
takes precedence over the existing task-keyed dict lookup; every call
site that doesn't pass one is unaffected. A resolved ref naming Ollama or
Llama.cpp is always constructible regardless of the session's configured
mode (neither needs credentials) - the realistic mixed local+cloud
comparison this ADR exists for. A ref naming a cloud provider other than
the session's currently configured one raises an actionable error rather
than silently falling back or reaching for credentials the request
snapshot was never given; genuine simultaneous multi-cloud-credential
routing is out of scope for this stage.

Test plan:
- backend/tests/test_model_routing.py (17 tests): the three auto policies
  including capability-filtering (never routes a vision requirement to a
  text-only model) and the ready/available filter; the full resolution
  chain's precedence order and its "explicit pins are never capability-
  filtered" posture; unified_catalog's aggregation and price annotation;
  api_provider.chat()/chat_stream() actually dispatching on a supplied
  model_ref (bypassing an intentionally-unconfigured task table), routing
  to Ollama while the session is in API mode, and the cross-cloud-mismatch
  actionable error.
- tests/test_node_state_migration.py: added the one new non-SceneNode
  "provider" access shape (ModelDescriptor, not SceneNode) the new test
  file's own iteration introduced.
- backend/tests/test_providers.py + test_api_provider_reasoning.py +
  test_backend_composer.py + test_agents.py: 337 passed, confirming the
  dispatch rewrite is behavior-preserving for every existing call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ADR-018 stage 18.2: node/branch model-override resolution chain

ChatState gains override_provider/override_model_id - an explicit model
pin, the input-routing opposite of the existing provider/model provenance
fields (which record what generated a completed reply, not what should
generate the next one). SceneDocument.set_model_override/
clear_model_override write it; resolve_model_for_node reads it back for a
node and, separately, for its branch root - mirroring _resolve_branch_
system_prompt's exact root-walk shape (get_branch_root, then read one
field off the root) rather than inventing a new inheritance mechanism.

AgentDispatcher._resolve_model_ref_for_dispatch computes node-override ->
branch-override (auto/workspace-default stay out of scope for this stage -
see the method's own docstring) and threads the result through the same
omit-when-None kwarg chain persona overrides already use: _dispatch ->
_call_chat_agent(_stream) -> ChatAgent.get_response -> ChatWorker.run ->
api_provider.chat/chat_stream(model_ref=...). No pin anywhere means the
kwarg is never even passed - every existing call site is unaffected.

Test plan:
- backend/tests/test_agents.py (7 new): resolve_model_for_node's
  precedence order (node pin beats branch-root pin) and root-walk
  correctness at every depth; sendMessage/regenerateResponse actually
  carrying the resolved ModelRef through the real dispatch pipeline into
  _call_chat_agent_stream; the negative case (no pin anywhere never adds
  a model_ref kwarg at all, not even as None).
- backend/domain/branches.py sanity-checked directly (set/clear/root-walk
  precedence) before writing the dispatch-level tests.
- Full suite from repo root: 2180 passed, 16 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ADR-018 stage 18.3 (backend half): wire schema, persistence, intents for the model-override pin

SceneNodeRow gains overrideProvider/overrideModelId (additive, "" default,
codegen regenerated), populated in scene_payload() from ChatState's own
fields. session_save.py/session_load.py round-trip them the same way
provider/model already do. Two new WS intents (setModelOverride/
clearModelOverride, backend/api/intents_model_routing.py) wrap
SceneDocument.set_model_override/clear_model_override in record_command -
classified "A" (undoable) in the ADR-010 close-out table, same posture as
setGroupColor.

Test plan:
- backend/tests/test_session_save.py + test_session_load.py: the pin
  round-trips through build_chat_data/restore_chat_payload; absent in a
  save (every pre-18.3 row) restores to "" / no pin, never a crash.
- backend/tests/test_canvas.py: the two intents actually mutate the node
  and publish through the real WS dispatch path.
- tests/test_node_state_migration.py: golden scene_payload() key-set
  snapshot updated (additive keys only).
- tests/test_undo_classification_gate.py +
  tests/undo_classification.py: both new intents classified and the
  locked registered-intent population count updated 140 -> 142.
- contracts/codegen.py --check: clean, two-field diff only.
- Full suite from repo root: 2185 passed, 16 skipped.

Frontend (badge + picker UI) is the remaining half of this stage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ADR-018 stage 18.3 (frontend half): model-override badge + picker UI

A distinct badge (📌, tinted not muted) renders whenever a node carries a
model pin - deliberately separate from the existing provenance badge, since
they answer different questions (what generated the last reply vs. what
pins the next one) and can legitimately co-occur.

The node menu gains "Pin to Current Model" (pins to whatever the Composer's
live route currently resolves to - reading composerStore.getComposer() at
click time, never subscribed, since only the click matters) and, when a pin
exists, "Clear Model Pin (<model>)". A full "browse the installed catalog"
picker needs a live catalog fetch this stage doesn't wire to the frontend -
"pin to current" is the honestly-scoped, fully-functional slice; see
ADR-018's own status note.

Wiring: sceneStore gains setModelOverride/clearModelOverride (plain
fireIntent wrappers, queueable like setGroupColor - both operations are
idempotent). SceneCanvas's stable per-node dispatcher gains the two
callbacks; getComposerRoute threads through as an optional getter
(App.tsx's real render supplies it, every other <SceneCanvas> render -
every existing test - keeps its harmless no-op default).

Test plan:
- ChatNodeView.test.tsx: badge absent/present/tooltip, co-rendering with
  the provenance badge, both menu items' click behavior including the
  current-route-unresolved no-op case.
- SceneCanvas.test.tsx: dispatcher wiring reads getComposerRoute at click
  time and calls the right store method; the no-op case when the route
  has nothing resolved yet.
- sceneStore.test.ts: both new methods send the right intent/args.
- Fixed 5 pre-existing test fixture builders (sceneStore.test.ts,
  SceneCanvas.test.tsx, ChatNodeView.test.tsx, renderCountGate.test.tsx,
  SceneCanvas.pinSearchJump.test.tsx, SceneCanvas.virtualization.test.tsx)
  that needed the two new SceneNodeRow/ChatNodeData fields to keep
  satisfying the (now stricter) generated wire schema/types.
- Full `npm run check` (schema + typecheck + lint + test + build +
  bundle-size): clean. 1582 tests passed across 60 files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ADR-018 stage 18.4: auto-policy setting, live dispatch wiring, Settings UI

Adds SettingsManager.get/set_auto_model_policy (persisted, closed-vocabulary
cheapest-capable/fastest/best-quality) and wires it into the auto rung of the
resolution chain: api_provider.chat()/chat_stream() now attempt an
auto-fallback via unified_catalog + the persisted policy at the exact point
they would otherwise raise "no model configured" - filtered to only
providers this session can actually dispatch to right now (both local
providers always; a cloud provider only when it is the session's live
credentialed one), so the auto rung can never hand back a ref
_provider_for_model_ref would then reject.

settings_manager threads down the same additive, omit-when-None kwarg chain
already built for model_ref (agents.py -> graphlink_chat_agent.py ->
api_provider.py). Adds the setAutoModelPolicy WS intent and a General-page
dropdown in the Settings dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-018 stage 18.5: fallback chains with visible substitution

Wraps api_provider.chat()/chat_stream() in a fallback-chain outer layer:
when a task in FALLBACK_ENABLED_TASKS (task_title, task_web_validate -
"naming/triage" per the ADR's own framing) hits a retryable/unavailable
failure after ADR-006's own same-provider transport retry is exhausted, one
more attempt fires against a different provider chosen by the same
auto-policy catalog stage 18.4 built, excluding the provider that just
failed. Correctness-sensitive tasks (task_chat, task_chart, ...) are
untouched - no fallback fires for them, matching "off by default for
correctness-sensitive tasks" from the ADR's decision #4.

The substitution is never silent: on_fallback threads down the same
additive kwarg chain as model_ref/settings_manager, and backend/agents.py's
dispatch surfaces it as a warning notification naming both the provider
that failed and the one substituted in. chat_stream's own fallback is
additionally guarded so it can only fire before any real text has reached
the caller - mirroring the existing "nothing forwarded yet" invariant the
transport-retry layer already established, so a partially-delivered reply
is never silently replayed against a different model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-018 review-fix: llama.cpp basename identity + settings_manager recursion leak

Two bugs surfaced by an adversarial review of the full 5-stage branch:

- unified_catalog() stored llama.cpp scanned models as their full scanned
  path, but _provider_for_model_ref's llama.cpp branch only ever accepts a
  model_id matching the BASENAME of a configured path (the same convention
  describe_active_model already uses - llama.cpp has no "load any installed
  model by id" catalog the way Ollama does). Every auto-pick or fallback
  landing on a llama.cpp candidate was therefore unconditionally rejected.
  Fixed by reducing to Path(...).name at catalog-build time.

- The "no model configured" auto-pick branches inside chat()/chat_stream()
  (now _chat_dispatch/_chat_stream_dispatch after 18.5's rename) recurse
  into the module-level chat()/chat_stream() names to dispatch the
  auto-picked ref - but that name now resolves to 18.5's own fallback
  wrapper, which had already popped settings_manager into a local before
  ever calling into the dispatch body, and the recursive call never
  re-included it. A failure on an auto-picked model (exactly the
  "nothing was configured" population FALLBACK_ENABLED_TASKS targets)
  could never trigger a further fallback attempt as a result. Fixed by
  re-including settings_manager in both recursive calls, in both
  functions.

Adds regression tests for both (pure-function catalog shape, live dispatch
through the real basename-matching path, and a live end-to-end trace
through the previously-broken recursion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
dovvnloading added a commit that referenced this pull request Aug 9, 2026
Recipes are data (the ADR's decision #4): named plans - goal, step
titles, default mode - that seed a build. The settings store gains
get_recipes/set_recipes (the get_mcp_servers posture exactly: JSON-safe
dicts, shared normalization so a round trip is always well-formed,
malformed entries dropped, whole-list replace). Two built-ins ship as
constants in backend/builder.py, merged read-only at list time so a
rename never desyncs a user's file.

builder/start gains an optional recipe argument: a recipe-seeded plan
lands its checklist immediately at awaiting_start with NO planning
model call (proven: the planner is monkeypatched to fail loudly and is
never reached). builder/listRecipes (request/reply) and
builder/saveRecipe complete the loop - save-your-build captures a
terminal plan node's goal + step titles (statuses deliberately dropped:
a recipe is the plan, not this run's history), refuses built-in names,
and replaces same-named user recipes. Classification gate 151 -> 153.

Launcher: a recipe picker (built-ins labeled) that enables launch
without a typed goal and relabels the action "Start from recipe";
PlanNodeView offers "Save as recipe" on done builds.

The 8.6 exit criterion is covered both ways in test_builder.py: a
shipped recipe and a user-saved build each seed a run through the real
WS intents. Full backend suite: 2378 passed (the 5 pre-existing
test_native_dialogs.py environment failures are untouched by this
branch); full frontend check green (1618 tests, bundle gate OK).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dovvnloading added a commit that referenced this pull request Aug 10, 2026
* ADR-017 stage 17.1: knowledge store schema, chunking, and ingestion pipeline

Adds the local SQLite knowledge store (documents/chunks, WAL mode, corrupt-db
rescue, backup cadence - mirrors chat_library.py's own connection hygiene),
structure-aware token-budgeted chunking with offset-exact citation support,
and an extract -> chunk -> store ingestion pipeline that reuses attachments.py's
pdf/docx/text extraction plus a new markup-stripping HTML extractor. Ingestion
is idempotent by content hash, scoped per collection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-017 stage 17.2: FTS5 lexical index and knowledge.search tool

Adds an FTS5 external-content index over chunks.text (insert/delete
triggers keep it in sync, migration backfills pre-existing rows), a
bm25-ranked search_chunks() with safe query sanitization against FTS5
operator syntax, and registers knowledge.search on ToolRegistry under a
new knowledge.read scope - auto-approval, read-only.

Tested via direct registry.invoke() calls: ADR-008's tool-use loop (the
piece that offers tools to a live model and processes ToolCallEvents) is
not built yet, so this tool is registered and fully working but not yet
wired into a live conversation - matching ADR-007's own established
precedent for capabilities built ahead of their eventual caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-017 stage 17.3: Provider.embed(), embedding cache, and vector search

Adds an `embedding` capability to ProviderCapabilities and a concrete
.embed() method on OllamaProvider (per-model probe via ollama.show(),
mirroring ollama_supports_tools) and OpenAIProvider (client-derived,
mirroring image_generation) - the local-first-default-plus-API-option pair
ADR-017 names. Anthropic/Gemini/llama.cpp declare embedding=False for this
stage.

The embeddings table (chunk_id, model_id) migration adds the vector index;
knowledge_embeddings.py owns the numpy pack/unpack and provider calls:
embed_pending_chunks() only ever embeds chunks with no cached vector for
that model (the exit criterion's "cache prevents re-embedding"), and
vector_search() is a brute-force cosine-similarity scan (numpy, already a
dependency - no sqlite-vec or new pip package) matching the ADR's own
"flat index file" alternative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-017 stage 17.4: hybrid retrieval fusion, budget-aware selection, untrusted context

Adds backend/knowledge_retrieval.py: reciprocal_rank_fusion() merges FTS5
and vector search by rank (never by raw score - bm25 and cosine similarity
aren't comparable scales), hybrid_search() runs both and fuses when an
embedding-capable provider/model is supplied, degrading gracefully to
lexical-only otherwise. select_within_budget() greedily trims a ranked
result list to a token allowance instead of a fixed k, so retrieval can
never overflow a small local model's context window. format_untrusted_
context() builds the labeled, instruction-resistant evidence block for
automatic chat-turn augmentation, reusing Web Research's own established
spotlighting convention with a distinct [k...] citation marker.

knowledge.search (stage 17.2's tool) now runs hybrid_search() when an
embedding provider/model is registered, unchanged (lexical-only) otherwise.
search_chunks()/vector_search() now also return each chunk's token_count so
budget-aware selection needs no second round-trip.

Fixture-set test proves the exit criterion directly: a query that only
lexical search answers, a paraphrase that only vector search answers, both
correctly resolved by hybrid_search() where either index alone misses one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-017 stage 17.5: sources (attachments/web-research/branch indexing) + citations UI

Backend:
- knowledge_ingest.ingest_text(): a sibling to ingest_file() for already-
  in-memory text with no file path to extract from - web-research retention
  and branch indexing both need this, not extract_text()'s file dispatch.
- Web Research retention: WebResearchRequest.retain_to_knowledge (opt-in,
  default False) makes WebResearchService.run() ingest each accepted
  source document via ingest_text() after synthesis - a retention failure
  is logged and swallowed, never breaks the actual research result.
- Branch indexing: ChatState.index_into_knowledge (per-node opt-in, set on
  whichever node's branch history should be indexed) + SceneDocument.
  set_chat_index_into_knowledge() + the new "knowledge" topic's two WS
  intents (backend/api/intents_knowledge.py): search (hybrid_search(),
  read-only, the frontend-reachable counterpart to backend/tools_knowledge.
  py's ADR-008-future tool) and setChatIndexIntoKnowledge (indexes the
  branch via chat_branch_history()+ingest_text() BEFORE flipping the flag,
  so a stored true always means the write actually happened).

Frontend: a new "Knowledge" search panel (AppBar chip + Dialog, mirroring
DiagnosticsDialog's request/reply shape) - search results render as
"N sources used", each expandable to the exact cited excerpt
(document[offsetStart:offsetEnd], byte-for-byte) with an Open source link
for real http(s) sources. No fabricated "jump to this local file at a byte
offset" mechanism - none exists anywhere in this codebase, so none is
claimed here either; the excerpt itself IS the content at that offset.

Contract: ChatState.index_into_knowledge -> indexIntoKnowledge (codegen
regenerated). Guard updates: node-state-migration wire-key list, undo-
classification intent count (143->145) + two new classifications.

Verified live: real WS round-trip through the Knowledge panel against a
freshly-started backend renders the correct empty-state UI with no console
or server errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-017 review-fix: guard embedding batch/dim mismatches, fix search race

A 5-dimension adversarial review of the ADR-017 diff surfaced 10 confirmed
findings. This fixes the six safely-scoped ones:

- embed_pending_chunks() now raises instead of silently mispairing
  vectors to chunk_ids when a provider returns a mismatched batch length.
- vector_search() validates query-embedding count and stored-vector
  dimension before np.stack(), turning a would-be opaque numpy crash into
  a clear, diagnosable error.
- KnowledgeSearchDialog's search request had no in-flight guard: the
  Enter-key handler could fire a second request while the first was still
  pending, and an out-of-order response would silently overwrite fresher
  results. A sequence token now discards stale responses, and the guard
  makes a second Enter press during an active search a no-op.
- The search input now has an aria-label, matching every other search
  input in the app.
- Both knowledge WS intent handlers (search, setChatIndexIntoKnowledge)
  now run their blocking SQLite calls via asyncio.to_thread instead of
  inline on the event loop, matching every other blocking-I/O intent
  handler in the codebase.

The remaining four findings are real but out of scope for a review-fix
pass and are documented as known gaps in the (local, gitignored) ADR-017
doc: OpenAIProvider's client-derived (not model-derived) embedding
capability check, the Ollama capability cache's pre-existing
permanent-negative-caching behavior, the WS search intent being
lexical-only in production pending a model-selection surface for
embeddings, and format_untrusted_context's lack of escaping (dormant,
matches Web Research's own already-shipped precedent).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.1: graph-mutation tools, tool-turn primitive, run_id stamping

The graph becomes a tool surface (the ADR's decision #1). Three pieces:

- api_provider.chat_turn_with_tools(): one model turn that RETURNS tool
  calls instead of dropping them - the exact gap ADR-007 left (the
  streaming dispatch loop never set ChatRequest.tools and silently
  discarded tool_call events). Mirrors _chat_stream_dispatch's provider
  construction, model_ref precedence, transport retry, and error
  translation; gates tools-capability authoritatively at construction;
  collects tool calls + final text + usage. No 18.5 fallback wrapper -
  a mid-build silent model swap is what ADR-018 rules out.

- backend/tools_graph.py: graph.create_node / graph.connect /
  graph.set_node_content (scope graph.mutate, approval "once") and
  graph.read_subgraph (graph.read, auto). Every mutating handler drives
  the same domain factories the WS intents call, wrapped in the same
  record_command - an agent-created node is undoable, patch-published,
  and persisted exactly like a user-created one. Placement is
  parent-relative and model-free; read results are excerpt-capped so
  reads don't eat the build's own token budget.

- run_id stamping: record_command() and composite() accept a run_id
  kwarg (Command.run_id existed since ADR-010 stage 10.5 but nothing in
  production ever assigned it). Handlers read run_id off the RunContext,
  per-call rather than via a long-open composite - the composite buffer
  is document-global, so holding one across an approval await would
  swallow concurrent user commands into the builder's undo entry.

Stage exit criterion covered in backend/tests/test_tools_graph.py: a
scripted agent turn through the real primitive and real registry creates
and connects two nodes, approval-gated (three prompts observed), and the
whole turn reverts as one undo_run while the user's own node survives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.2: run_node - a node's own action as a builder tool

run_node(node_id, action?) executes a node's action INLINE under the
builder's run - deliberately not re-entering the fire-and-forget
AgentDispatcher surfaces (own busy kinds, intent-wired callbacks, no
awaitable completion). Actions:

- execute (pycoder default): runs the node's current code in the SAME
  REPL a manual Run uses (dispatcher.get_pycoder_repl), under the same
  240 s timeout, landing results through the same complete_pycoder_run /
  fail_pycoder_run domain methods - the node renders identically to a
  manual run. No analysis turn: the builder model is the analyst; it
  reads the output in this same loop.
- reply (chat default): generates an assistant reply child from the
  node's branch history via _call_chat_agent, with branch System-Prompt
  resolution and provider/model provenance stamped, recorded as a
  run_id-stamped command.
- chart (explicit, on any content node): chart generation is an action
  ON a source node, not a node kind's own run - dispatching purely on
  kind would let the chat action shadow it, which the first test run
  caught. Generates via _call_chart_agent into add_chart_node.

Scope enforcement is dynamic per action (execute -> code.execute,
reply/chart -> provider.call) inside the handler - the ADR's "run_node
additionally carries the scope of what it runs" - since the registry's
own scope check is static per-tool. The target node carries the
builder's request_id as pending_request_id for the run's duration:
per-node conflict guards, the live-run undo refusal, and the spinner UI
all come free. web_research runs are a named not-yet error until the
network-gating stage.

ToolRegistry.invoke now re-raises RequestCancelledError from HANDLERS
instead of swallowing it into an error ToolResult - a long-running
handler observing the same cancel event must follow the same contract
as invoke's own checkpoints (cancellation propagates to the loop, never
fed back to the model as a tool "error" to reason about).

Stage exit criterion covered in backend/tests/test_run_node_tool.py:
the scripted agent turn runs a code node for real (fake REPL) and the
tool result it reads back carries the execution output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.3 (backend): plan node kind + the Builder loop

The plan node - the ADR's "planning is explicit and visible" decision -
lands end-to-end: PlanState (goal/steps/status machine/budgets/spent
counters/approval surface, all plan_-/builder_-prefixed so the
bare-attribute ban needs zero exemptions), add_plan_node/set_plan_steps
domain mutators (completed steps are immutable history - neither a user
edit nor a model replan can rewrite them), 15 wire fields + a typed
PlanStepRow (codegen regenerated), session save/load with the one
load-time normalization: a non-terminal builder_status restores as
"interrupted" - terminal, honest, resumable - since no RunHandle
survives a restart.

backend/builder.py is the loop itself: planning via respond_json
(provider-universal, works where the executor's tools-capable gate would
refuse), then per pending step a bounded turn loop alternating
chat_turn_with_tools with registry invocations. Loop control is in-band
through four auto-approval builder.* tools (complete_step / replan /
finish_build / abort) - a per-step turn cap stops a step that never
completes. Budgets are hard: tokens/wall-clock checked before every turn
and every tool call, the step budget at the outer pre-start point
(spent_steps increments at step START, so an in-flight step must not
trip its own check - caught by this stage's own test). A breach pauses
with all state on the plan node; resume is canvas-sourced.

The approval router is mode-aware: copilot parks every non-auto call on
the run's approval_future (the pycoder swap-per-round mechanics) behind
plan-node awaiting/summary fields; autopilot auto-approves calls whose
registered scopes fit the disclosed set and still prompts for net.fetch.
ToolRegistry gains scopes_for() so the router keys on registration truth.

AgentDispatcher.start_builder_run claims the new "builder" kind (Stop =
release-on-cancel + finalize landing "stopped", the 6.2 posture) and
lazily builds the session's ToolRegistry - ADR-007's registry finally
gets its first production constructor. Six new intents (builder/start|
startExecution|cancel|approveTool|denyTool + scene/setPlanSteps),
classified, gate 145 -> 151; builder/start is A - it genuinely records
the plan-node creation, stamped post-claim with the run id so undo_run
reverts the plan node too. Planner/executor prompts join the pinned
registry inventory (terse by design - every token recurs per turn; the
executor carries the untrusted-content spotlighting language).

Stage exit criterion covered in backend/tests/test_builder.py: a
scripted co-pilot build lands a 4-node branch with every mutating call
individually approved, control tools never prompting, budgets/replan/
abort/deny paths each proven, Stop verified as slot-release-immediate,
and the whole build reverting via one undo_run. Mid-step replan keeping
the running step live (and the loop re-resolving it from the replaced
list) was a real bug this suite caught before it shipped.

Frontend (PlanNodeView + launcher) follows in the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.3 (frontend): PlanNodeView + Builder launcher

PlanNodeView renders the build live off the wire row: goal, the
checklist with per-status markers and step summaries, the three budget
gauges (spent/max steps, tokens, seconds), status + detail (failed
details are role=alert), the autopilot chip, and mode-appropriate
controls - Start build / Resume for awaiting_start|paused|interrupted,
Stop while running. The tool-approval panel copies
CodeExecutionApprovalPanel's architecture: per-node, zero passive
dismissal (a dismissal would strand the run's parked approval future),
and zero-argument Approve/Deny closed over the current snapshot's
pendingRequestId.

BuilderLaunchDialog (AppBar, next to Knowledge) collects goal, oversight
mode, and the three hard budgets, and starts the build through the
value-returning builder/start intent - selecting autopilot surfaces the
disclosure sentence inline before launch (per-run, disclosed choice per
the ADR). Six new sceneStore methods wire the run controls.

Fixture ripple: the 15 new required wire fields land in the five
SceneNodeRow fixture builders (the 17.5 indexIntoKnowledge precedent).
Styling uses only the design-system variables - the no-raw-colors gate
caught the first draft's hex fallbacks.

Verified: 10 new component tests; full frontend check green (1612
tests, typecheck, lint, build, bundle gate); full backend suite green
(2364 passed; the 5 pre-existing test_native_dialogs.py environment
failures are untouched by this branch).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.4: the Undo build affordance

"Undo this build" closes the loop ADR-010 stage 10.5 left open: the
undo_run machinery and the scene/undoRun intent shipped with zero
callers, and stage 8.1 made the builder stamp every command with its
run id - this adds the affordance. PlanNodeView offers an "Undo build"
button once the run is over (done/failed/stopped/interrupted/paused,
with a stamped run id), wired through the existing sceneStore.undoRun.
The domain's live-run guard already refuses undo mid-run, so
Stop-then-undo remains the enforced sequence with no new code.

Live browser verification caught a real bug: the plan node's buttons
lacked the `nodrag` class, so React Flow's drag handler swallowed every
click (the ChatNodeView-pinned convention). Fixed on all five buttons
and pinned by a new test. Verified end-to-end in the running app: a
build launched from the AppBar dialog landed a plan node (with the
Ollama-unreachable failure surfacing honestly as role=alert), and Undo
build reverted the run-stamped creation.

The stage's revert-the-whole-build exit criterion is covered by
test_builder.py's exit test (undo_run reverts a 4-node scripted build,
the user's own node surviving).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.5: research via run_node, autopilot net gate, MCP wiring

run_node gains the "research" action (web_research's default): the SAME
sync pipeline the dedicated surface runs - WebResearchService with all
of ADR-004's SSRF/IP-pinning/robots machinery inside its fetcher -
landing through the same complete/fail domain methods, inline under the
builder's run. The builder's threading.Event cancel bridges onto the
service's own CancellationToken via a watcher task (the pipeline stages
checkpoint on the token, not our event) - proven by a test that hits
Stop mid-run and watches the token trip.

Designing it surfaced a real autopilot hole: the mode router keyed
auto-approval on a tool's REGISTERED scope, and run_node registers only
graph.read - autopilot would have silently auto-approved a net.fetch
research run. run_node_effective_scope() now derives the scope a call
actually exercises (target kind + action) and the router unions it in;
malformed run_node calls route to the human. Two tests pin the exit
criterion's "no network unless approved": a net.fetch tool prompts in
autopilot, and run_node(research) prompts via the derived scope while
graph mutations still auto-approve.

ADR-007's deferred MCP runtime wiring lands in its designated consumer:
builder_tool_registry reads the persisted server list, connects each
enabled server, and registers its tools (namespaced, per-server scopes,
approval="always") with per-server failure tolerance - one broken
config never costs the Builder its graph tools.

The interrupted-on-load normalization (shipped in 8.3's restorer) gets
its round-trip tests: a "running" build restores as interrupted with
its mid-flight step failed - never a spinner no run backs - while
terminal states and spent budgets round-trip verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 stage 8.6: recipes and save-your-build

Recipes are data (the ADR's decision #4): named plans - goal, step
titles, default mode - that seed a build. The settings store gains
get_recipes/set_recipes (the get_mcp_servers posture exactly: JSON-safe
dicts, shared normalization so a round trip is always well-formed,
malformed entries dropped, whole-list replace). Two built-ins ship as
constants in backend/builder.py, merged read-only at list time so a
rename never desyncs a user's file.

builder/start gains an optional recipe argument: a recipe-seeded plan
lands its checklist immediately at awaiting_start with NO planning
model call (proven: the planner is monkeypatched to fail loudly and is
never reached). builder/listRecipes (request/reply) and
builder/saveRecipe complete the loop - save-your-build captures a
terminal plan node's goal + step titles (statuses deliberately dropped:
a recipe is the plan, not this run's history), refuses built-in names,
and replaces same-named user recipes. Classification gate 151 -> 153.

Launcher: a recipe picker (built-ins labeled) that enables launch
without a typed goal and relabels the action "Start from recipe";
PlanNodeView offers "Save as recipe" on done builds.

The 8.6 exit criterion is covered both ways in test_builder.py: a
shipped recipe and a user-saved build each seed a run through the real
WS intents. Full backend suite: 2378 passed (the 5 pre-existing
test_native_dialogs.py environment failures are untouched by this
branch); full frontend check green (1618 tests, bundle gate OK).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ADR-008 review-fix: close autopilot scope gap, fix budget/replan bugs, canonicalize chart data

Fixes 13 bugs found by re-reading the branch's own diff after an adversarial
review pass: autopilot treated an empty MCP tool scope set as auto-approved
(any unscoped server bypassed human approval entirely); Ollama/Anthropic/
Gemini all dropped already-collected token usage on tool-call turns, so the
builder's token budget went effectively unenforced; a second plan replan
could mint a step id that collided with one an earlier replan already used,
killing the build with an unresumable error; a watchdog timeout or any other
provider exception left the in-flight step wedged at "running" forever, and
"failed" was not a resumable status even though the plan node's state lives
entirely on the canvas; a budget breach discovered on a later tool call in a
turn could stomp a step an earlier call in that same turn had just completed,
or silently drop a declared finish; graph.read_subgraph had no cap on node
count, so a hub node's full read could overflow a turn's context window; a
transient Ollama capability-probe failure was cached as a permanent negative,
silently blocking the builder forever with a false "no tool support" error;
the run_node schema still advertised research as unsupported; the web
research service's own cancellation exception was swallowed into an ordinary
tool error instead of propagating as a real cancellation; chart generation
skipped canonicalize_chart_data before storing the model's raw output,
violating the chart state's documented invariant; deleting a plan node with
a live builder run never cancelled it, permanently locking the builder for
the rest of the session; and undo/redo of a plan-node command recorded
mid-run could resurrect a stale "running"/"awaiting_approval" status with no
live run behind it.

Also splits register_node_intents's live-run teardown capture into its own
function to stay under the register* 300-line cap after the new plan-cancel
branch pushed it over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants