Skip to content

Safe macro scripting: preservation → interpreter → capability broker → signatures → network - #36

Merged
kevincarlson merged 71 commits into
mainfrom
claude/safe-macro-implementation-circn5
Jul 24, 2026
Merged

Safe macro scripting: preservation → interpreter → capability broker → signatures → network#36
kevincarlson merged 71 commits into
mainfrom
claude/safe-macro-implementation-circn5

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

Implements the macro-scripting suite per LOKI_MACRO_SCRIPTING_SPEC.md, ADR-0014 (signatures/trusted publishers) and ADR-0015 (network capability).

67 commits. The organising principle throughout: macros are disabled by default and the interpreter has no authority of its own — every effect passes through a capability broker, and nothing in a document can grant itself trust.

What's here

Phase Summary
1 Payload preservation. MacroPayload on the provenance layer (not the CRDT); DOCX/XLSX VBA and ODF Basic/+Scripts/ round-trip byte-identically; "macros dropped" warning on lossy conversion; passive infobar.
2 loki-basic — pure-Rust tree-walking interpreter for VBA + StarBasic over one AST. No JIT (iOS-safe), fuel-metered, no ambient I/O. Conformance suite + 3 fuzz targets + a CI purity gate.
3 loki-vba — MS-OVBA decompression, CFB/dir-stream parse, source extraction, p-code stomping heuristic. Read-only macro viewer.
4 loki-macro-host — closed capability catalog, trust store keyed by payload hash (T10: trust never inferred from content), capability broker, anti-spoof permission prompts.
5–6 Object-model facade, edits batched to one undo entry, worker-thread runner with live prompts + always-available Stop, auto-run gating behind a separate token, spreadsheet UDFs (compute-only → #MACRO!), MACROBUTTON click-to-run.
7 Macro editor with source-only write-back (p-code dropped so Office/LO recompile — closes T5).
7B Picker-mediated file access: OpenFileForReading/OpenFileForWriting. No path-addressed API exists — the user's pick is the grant (T3).
8A Signature verification (loki-macro-sig, L2): CMS/PKCS#7 + X.509, ODF XMLDSig with an inclusive C14N implementation, RFC-3161 timestamps, trusted-publisher pinning. Verification proves authorship; only a user-pinned publisher yields trust.
8B Network capability: origin-scoped, session-max grants (never persisted), reqwest/rustls transport behind an off-by-default build feature and a per-document runtime opt-in.

Security posture

  • Deny by default at every layer. Build feature AND runtime setting AND per-origin prompt are all required before a macro reaches the network; file access requires a capability grant AND a picker pick.
  • No path-addressed filesystem, no process spawn, no FFI/COM — these aren't gated, they don't exist.
  • Non-interactive contexts are always refused, enforced two ways: no server/headless crate links loki-macro-host at all (CI gate), and every non-interactive run uses a backend whose effects refuse.
  • Trust is keyed by payload hash — editing a document's macros externally drops trust by hash mismatch.
  • Redirects are re-gated per hop against the granted origins; header deny-list strips ambient credentials (an author-set Authorization is deliberately kept — it's the author's own).

Testing

~400 tests across the macro crates: unit, end-to-end runs against a mock backend, threaded worker↔UI bridge tests, and 6 fuzz targets. Both macro-net feature configurations pass clippy --all-targets -D warnings and the full suite; all 8 CI gate scripts green.

Known gaps — deliberate, documented, not blocking

  • Signature corpus validation. Every signature test builds its fixture in-process, which proves self-consistency but not agreement with Word/LibreOffice. Four gates stay open on this; VBA signatures therefore read Unsigned rather than a misleading Invalid. The gathering checklist is docs/macro-signature-corpus.md — it deliberately warns against implementing the content-hash from the spec alone, since an unvalidated normalisation would report every real signed document as tampered.
  • Macro-editor caret/IME rendering needs a windowed build; checklist in docs/macro-editor-verification.md.
  • Smaller items carry in-code TODO(<topic>) markers: cross-run session-origin memory, per-run network timeout, punycode/homograph origin display, HttpPost (deferred by ADR-0015).

Review notes

  • Two review passes ran over this branch; the second found 7 issues (handle-range collision, an origin-normalisation mismatch between the grant check and the redirect resolver, a non-retryable failed .Close, an opaque platform token exposed as a macro-visible path, unbounded response retention, a stale network opt-in surviving a trust downgrade, one stale TODO) — all fixed in 59edb67 with regression tests.
  • The branch was rebased onto current main mid-development, so the diff is macro work only.
  • docs/fidelity-status.md §13 is the living registry for what's wired vs. pending.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB


Generated by Claude Code

claude added 30 commits July 23, 2026 12:01
Defines the security-first design for macro support: disabled-by-default
trust model keyed on a local trust store, capability-gated execution
through a pure tree-walking interpreter (no JIT, iOS-compatible), a
permanent 'never' list of refused features (process exec, FFI, COM/UNO,
path-addressed file I/O, p-code, XLM), payload preservation in the
provenance layer (fixing today's silent macro stripping on save), and an
8-phase implementation plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds MacroPayload to the doc-model provenance layer (DocumentSource.macros)
with an order-independent, content-addressed payload hash for the future
trust store. Teaches DOCX and XLSX import to collect vbaProject.bin (+ Word's
vbaData.xml) into the payload, and export to re-emit it verbatim only for
macro-enabled kinds (.docm/.dotm/.xlsm), stripping it for plain .docx/.xlsx
to match Office extension semantics. VBA bytes are never parsed or executed.

- loki-doc-model: io::macros (MacroPayload/PreservedPart/MacroPayloadKind)
- loki-ooxml: shared crate::vba collect/emit; DocxKind macro variants +
  DocxMacroEnabledExport/TemplateExport; XlsxImport::run + XlsxImportResult.macros
  + XlsxExport::export_with_macros
- Relocated cell_ref_to_coord to import_worksheet.rs (file-ceiling)
- Round-trip tests: byte-identical project preservation, stable payload hash,
  plain-export stripping

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Teaches OdfPackage to collect the Basic/ and Scripts/ subtrees (manifest-
driven, preserving each entry's declared media type and directory entries)
into a MacroPayload (kind OdfBasic) on document.source.macros. ODT export and
ODS export re-emit the libraries verbatim plus their manifest file-entries via
a shared script_write helper. Scripts are never parsed or executed.

- loki-odf: package_scripts collector; OdfPackage.macros; script_write shared
  re-emit helper; OdtImporter/OdtExport wired; OdsImport::run +
  OdsImportResult.macros + OdsExport::export_with_macros
- Round-trip test: byte-identical StarBasic module + stable payload hash

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
loki-convert now surfaces a 'macros dropped' warning when a source's VBA or
StarBasic payload cannot be carried into the chosen target format (spec §3.5),
and threads the payload through the identity ODS->ODS path so same-family
spreadsheet macros are preserved rather than dropped. import_sheet uses the
new XlsxImport::run / OdsImport::run to retrieve the payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
…itor

Adds the passive, non-modal AtInfobar component (appthere-ui) and the macros
Fluent domain (macros.ftl, registered in loki-i18n DOMAINS). loki-text shows
'This document contains macros. Macros are disabled.' when an opened document
carries a preserved payload (editor_macro_notice reads document.source.macros,
which lives outside the Loro CRDT). No enable/execute action exists in Phase 1
by design. Also documents the whole Phase 1 surface in fidelity-status.md §12.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Backticks StarBasic in doc comments (clippy::doc_markdown), applies cargo fmt
across the new macro code, and records the whole Phase 1 surface in
docs/fidelity-status.md §12. All touched crates pass cargo test, clippy
-D warnings, and the license/ceiling/unsafe CI gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
New L1 crate loki-basic: the pure tree-walking BASIC interpreter core (macro
spec Phase 2). No JIT, no I/O deps, forbid(unsafe_code) — the interpreter has
zero ambient authority (Host trait seam). This commit lands the foundation:

- Dialect flag (Vba/StarBasic), typed errors (BasicError/RuntimeError with
  VBA Err.Number; feature-refusals and fuel/cancel stops are untrappable),
  Host trait + FuelVerdict + NullHost/FuelBudget (fuel metering, spec §8)
- Lexer: line-oriented, case-insensitive; numbers (int/float/hex/oct/exponent),
  strings with "" escaping, #date# literals, operators, statement separators,
  line continuations, comments, type-suffix handling; 21 tests
- Registered in workspace members + dependency-direction gate (L1 leaf)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Full AST (expressions, statements, declarations, module/procedure structure)
and a recursive-descent parser over the lexer, shared by both dialects:

- Pratt expression parser with VBA precedence (^ binds tighter than unary
  minus; concat above comparison; And/Or/Xor/Eqv/Imp/Mod/Is/Like keywords)
- Statements: Dim/ReDim/Const, Let/Set assignment, bare + Call + named-arg
  calls, If (block + single-line + ElseIf), For/For Each, Do/Loop (pre/post
  While/Until), While/Wend, Select Case (values/ranges/Is), With + leading-dot,
  Exit, GoTo/labels, On Error, Resume, Error, Stop/End
- Declarations: Sub/Function/Property with ByVal/ByRef/Optional/ParamArray/
  defaults, Type, Enum, module Const/Dim, Option Base/Explicit/Compare,
  Attribute VB_Name; Declare (FFI) captured for runtime refusal (spec §7)
- 17 parser integration tests + lexer split (scan.rs) to hold the ceiling

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The dynamic Value type (Empty/Null/Bool/Int/Long/Double/Str/Date/Array) with
VBA/StarBasic semantics:

- Coercions: CBool/CDbl/CStr/CLng with banker's rounding, Bool as -1/0, strict
  numeric-string parsing
- Arithmetic: numeric promotion (Integer<Long<Double), overflow raises error 6
  (no silent widening), / always Double, \ and Mod truncate, ^ Double, the +
  string-concat vs numeric overload
- Null propagation (& treats Null as ), comparison (Option Compare Text),
  logical/bitwise And/Or/Xor/Eqv/Imp/Not, and a Like matcher (?/*/#/[a-z]/[!…])
- Value-typed arrays with multi-dim row-major indexing, LBound/UBound, and a
  16M-element cap (spec §8 memory guard)
- 19 operator tests + coerce unit tests; crate-level allows for intentional
  numeric casts and exact-zero float guards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The evaluator that runs parsed modules against a Host, fuel-metered end to end:

- Interp: indexes procedures/consts/enums/module vars; call() entry; per-step
  fuel charge (loops charge per iteration so empty infinite loops still stop);
  256-deep call-stack cap
- Frames on the Rust stack (env.rs) so &mut self + &mut Frame coexist; case-
  insensitive vars; Err object + On Error handler state + resume point
- Expressions (eval.rs): literals, vars/consts/globals, zero-arg function
  auto-call, arithmetic, array indexing; Member/New/With deferred to Object phase
- Statements (exec.rs): label/GoTo/On Error/Resume body loop, assignment
  (var + array element), Dim/ReDim(+Preserve 1-D), Const, bare/Call/Debug.Print
- Control flow (exec_block.rs): If, For/For Each, Do (pre/post While/Until),
  While, Select Case (values/ranges/Is), With
- Calls (call.rs): positional/named/Optional/ParamArray binding + ByRef
  copy-in/copy-out for lvalue args; End/Stop halt sentinel
- Built-ins (builtins/): ~40 pure functions (math, conversion, strings, array/
  info) with no host access; is_builtin gate; date-serial helper
- 20 end-to-end conformance tests (recursion, ByRef, On Error, fuel exhaustion,
  overflow, Select, For Each, ...)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Rounds out Phase 2's verification (spec §12):

- Purity gate (scripts/check-loki-basic-pure.py, wired into CI): asserts
  loki-basic depends only on its allow-list (no I/O — every effect via the Host
  trait) and that no server/headless crate links the interpreter (spec §10)
- cargo-fuzz harness (loki-basic/fuzz/, detached from the workspace): lex/parse/
  interp targets for parser-hardening (T9)
- In-tree panic-freedom smoke tests over adversarial input (runs in normal CI)
- Split exec.rs → exec_dim.rs to hold the 300-line ceiling
- Baselined the deliberate suppressions (optional-token ,
  documented numeric-cast/dead-code allows); dropped the resolved loki-odf entry
- fidelity-status.md §12 updated: interpreter core = Partial (Phase 2 landed),
  in-app execution surface still deferred to loki-macro-host

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
…-code)

New L2 crate loki-vba reads module source text from vbaProject.bin for the
macro viewer, under the hard rule that compiled p-code is never parsed or run
(macro spec §4.4, threat T5 — VBA stomping):

- MS-OVBA decompression (§2.4.1) in pure Rust: LZ77/RLE with per-chunk 4096
  cap + 64 MiB global bomb guard; literal/copy/overlap paths tested
- CFB walk (cfb crate) locates the /VBA storage by its dir stream; dir-stream
  parser (§2.3.4.2) reads code page + per-module name/stream/offset/type,
  skipping unknown records by size
- Per-module: decompress from MODULEOFFSET only (the compiled cache before it
  is ignored), decode via the project code page (encoding_rs), normalise CRLF
- Tamper heuristic: empty source + substantial p-code → 'possible VBA stomping'
  warning; stomped modules read as empty (inert here — Loki runs no p-code)
- Every failure is a typed VbaError, never a panic (§12, T9); malformed input
  degrades to unreadable
- End-to-end tests synthesize a real compressed vbaProject.bin and read it back
- Registered in workspace members + dependency-direction gate (L2 leaf)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Visibility before executability (spec §9.6): users can now inspect what a
document's macros would do, read-only, before any trust decision.

- loki-odf::basic — extract StarBasic modules (name + source) from the
  preserved Basic/ XML parts; skips library indexes and dialogs
- loki-text macro viewer: the macros infobar gains a 'View macros…' action
  opening a read-only, monospace source panel (module tabs + selected source),
  surfacing the VBA-stomping tamper warning. Source is extracted on demand
  (VBA via loki-vba source-only; StarBasic via loki-odf), so the infobar stays
  a cheap presence check. Mount stays within editor_inner's 1-line budget
  (present:bool → ctx:MacroCtx, Arc-identity PartialEq)
- macros.ftl: viewer strings (title/close/empty/unreadable/tamper/view-action)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
…status

- cargo-fuzz harness for loki-vba (decompress/read targets; detached workspace)
- in-tree panic-freedom smoke tests over adversarial/truncated OVBA + CFB input
  (bomb-guard bound, no hang/panic)
- unsafe-policy gate skips cargo-fuzz harnesses (bin-only, no src root) — also
  clears the latent loki-basic-fuzz discovery
- fidelity-status.md §12: VBA/StarBasic source extraction + read-only viewer
  marked landed; in-app execution surface still deferred to loki-macro-host

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the security spine for macro support (macro spec §2, §5, §7 — no
execution yet; that is Phase 5). New L5 crate loki-macro-host:

- Capability: closed catalog (§5.2) — DocRead baseline, DocWrite/UiDialog/
  Clipboard{Read,Write}/File{Read,Write}/Print prompt, Network refused in
  v1. GrantScope Deny/AllowOnce/AllowSession/AlwaysForDocument (no
  all-documents scope). CapabilityDecision Granted/Prompt/Denied/Refused.
- TrustStore: per-user, local, JSON-persisted, keyed by the macro-payload
  hash (§2.4). Nothing in a document can influence its own trust (T10):
  a fresh store trusts nothing, and editing the macros changes the key so
  trust drops. Session-only trust never reaches disk; a corrupt store
  degrades to empty so it can't block opening.
- CapabilityBroker: the loki-basic Host impl (§4.3, §5.1) — pure evaluate()
  decision surface plus fuel metering and a shared cancel flag (§8). UDFs
  run compute-only (§6.3): every effect denied, never prompts.
- MacroService: cheap-clone Arc handle (SpellService pattern) wrapping the
  store + per-document session state; trust decisions, capability grants,
  revocation, forget, and Document Security summaries for the panel.

Registered in the workspace, the dependency-direction gate (L5), and the
interpreter-isolation gate. Baselines the two Phase-3 loki-vba fuzz targets
(pre-existing panic-freedom `let _ =` harnesses) in the suppression ratchet.

40 unit tests: capability matrix, grant-scope honouring, immediate
revocation, T10 trust-forgery, and persistence round-trips all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the macro-originated dialog surface (macro spec §5.5, §2.3, §5.4),
rendered in a visually distinct frame that app chrome never uses so a
malicious macro cannot impersonate a genuine app dialog (threat T7):

- New reserved accent token COLOR_MACRO_BADGE (violet), documented as
  never-for-chrome.
- MacroDialogFrame: badged "Macro: <project>" header + document title over
  a violet-bordered card on a dimming backdrop (same positioned-ancestor
  contract as AtConfirmDialog).
- AtMacroTrustDialog: the three §2.3 choices (Keep disabled / Enable for
  session / Trust). AtPermissionPrompt: a first-use capability prompt with
  Deny as the default and Allow once/session/always.

Both stay appthere_ui-pure — display strings in, an abstract choice enum
(MacroTrustChoice / MacroGrantChoice) out — so the crate takes no
macro-host or document dependency; the app maps the choice.

macros.ftl gains the badge word, permission-prompt buttons, per-capability
names/consequences (keyed by Capability::id()), and Document Security panel
strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Completes Phase 4 by connecting the trust store and capability broker to
the app (macro spec §9). Still no execution surface — this records the
decision only; Phase 5 wires running a macro.

- app.rs provides MacroService into context (SpellService pattern), loaded
  from the suite data dir via a new loki_app_shell::app_data module that
  centralises the per-user data-dir convention (spell cache + macro trust
  store now share one root; data_root's cfg split drops a stale
  #[allow(unreachable_code)]).
- editor_macro_notice is now trust-state aware: the infobar offers
  "Enable options…" (opens AtMacroTrustDialog) + "View macros…" when
  disabled, and switches to a session/trusted note with "Document
  security…" once enabled; choices are recorded through MacroService.
- editor_macro_security_panel: the Document Security panel (§9.4) — trust
  state, granted capabilities with immediate Revoke, the auto-run-on-open
  opt-in (§5.6), and "forget this document".
- AtInfobar gains an optional secondary action so both Enable and View fit.

fidelity-status §12 updated: Phases 1–4 implemented; execution (Phase 5)
not started. Workspace clippy -D warnings, fmt, and all CI gates green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Extends the interpreter from a pure calculator to a capability-gated
execution engine (macro spec §4.3, §6, §7) — still routed entirely through
the Host seam, so it has no ambient authority of its own.

- Host trait gains the effect surface: get_root / get_member / set_member
  (object model) and dialog (MsgBox/InputBox), each defaulting to deny so
  NullHost/FuelBudget stay pure calculators.
- Value::Object(ObjectRef) flows through variables, Set, and With; the Is
  operator compares object identity (and `x Is Nothing`). Coercions treat
  an object as a type mismatch; IsObject/TypeName handle it.
- Interpreter dispatch: Member get/method-call/property-set to the host,
  object-model root resolution (Application/ActiveDocument/…), WithContext,
  and MsgBox/InputBox routed to Host::dialog. The built-in Err object
  (Number/Description/Clear/Raise) is wired.
- "never" list (§7): Shell, CreateObject/GetObject, createUnoService, Kill/
  Dir/MkDir/…, GetSetting/SaveSetting/Environ, DDE*, OnTime/Timer, and
  Declare'd FFI names raise an UNTRAPPABLE feature_refused (1004) — bare or
  as a member — so On Error Resume Next can't slip past them.

Tests: object-model dispatch + dialogs (host_tests, 12), one assertion per
§7 row incl. untrappability (refusal_tests, 12), and the now-wired Err
object. loki-basic stays zero-I/O (purity gate green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The capability broker now drives real execution (macro spec §5, §6).

- ExecutionHost implements loki-basic's Host by composing the
  CapabilityBroker (fuel + cancel + decisions), a MacroBackend seam (the
  app's prompt + dialog surface), and a document facade. Every effect flows
  through gate(): Granted proceeds, Prompt asks the backend, Denied → a
  trappable "permission denied" (70), Refused → an untrappable
  feature-refusal.
- Object-model facade v1 (§6.1): Application.Name/Version, ActiveDocument →
  Document with Name/Text/Content/ParagraphCount (DocRead), AppendText/
  InsertText/TypeText + Text= (DocWrite), PrintOut (Print). Reads are
  baseline; writes accumulate into one EditBatch.
- EditBatch (§6.2): every DocWrite is recorded; the app applies the whole
  batch as one Loro transaction = one undo entry.
- MacroRuntime::run executes a NAMED procedure only — no auto-open/event
  discovery (§5.6), closing the T1 vector by construction. Returns the batch
  and a typed MacroRunError (is_refusal / is_resource_stop). list_procedures
  feeds the Tools ▸ Macros picker. DenyBackend = compute-only (UDF posture).

Exit-criteria tests (runtime_tests, 12): "never" table inert through the
host, auto-open never fires, DocWrite denied is trappable + makes no edits,
a granted multi-write run is one batch, and an infinite loop is fuel-stopped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Records Phase 5 (engine) status in §12: interpreter core now Yes (effect
surface wired), the "never" list and execution engine + facade rows added,
and the in-app Tools ▸ Macros runner marked as the remaining tail —
worker-thread + Stop + Loro-grouped-undo application need GUI verification
and are landed in a follow-up rather than shipped blind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Wires the execution engine into loki-text so an enabled document can
actually run its macros (macro spec §6, §9.3).

- editor_macro_apply: applies a run's EditBatch to the live Loro document
  with the same section-0 text/block primitives the editor uses, then
  commits ONCE — and one commit is one undo checkpoint, so a whole macro run
  is exactly one ⌘Z (spec §6.2). Unit-tested against a LoroDoc.
- editor_macro_run: reads the document body, resolves capabilities from the
  trust record (MacroService.grant_set_for), runs the named proc via
  MacroRuntime, and applies edits on success. RunnerBackend collects
  MsgBox/InputBox text into an output log. Refusal/denial/resource-stop map
  to plain-language guidance. End-to-end tested (granted write reaches Loro;
  refused/denied make no edits).
- editor_macro_runner: the runner panel — lists runnable procedures across
  modules and runs a chosen one; reachable via "Run a macro…" in the
  Document Security panel. macros-run-* i18n strings added.

v1 posture (deliberate — ships without an un-verifiable async UI): grants
are pre-resolved (no mid-run prompts; grant in Document Security and re-run),
dialogs are logged rather than shown as blocking modals, and the object
model is plain-text. Interactive prompts + live modals + a worker-thread
Stop, and rich-text/spreadsheet writes, are the documented follow-on
(fidelity-status §12). Workspace clippy -D warnings, fmt, and all gates green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Replaces the pre-resolved synchronous runner with the full interactive
layer (macro spec §5.4, §5.5, §8): the macro runs on a worker thread while
the UI stays responsive.

- editor_macro_bridge: the worker↔UI bridge. The BridgeBackend (MacroBackend)
  posts each capability prompt / dialog over a `futures` unbounded channel
  and blocks the worker on a per-request std-mpsc reply. Sequential by
  construction (single-threaded interpreter). Cancel-aware: Stop trips the
  shared flag and, if the worker is blocked on a prompt, the UI answers it
  deny/cancel so the next fuel step aborts; a gone UI degrades to deny.
  Proven with threaded tests (grant/deny/dialog/Stop-loop/Stop-during-prompt).
- editor_macro_prompt: renders the live prompt in the anti-spoof frame —
  AtPermissionPrompt for capabilities, MacroDialogFrame for MsgBox/InputBox
  (with a text field). Grants the user allows persist to the trust record.
- editor_macro_runner(+_ops): dioxus::spawn a worker running MacroRuntime with
  the bridge backend; drain prompts via StreamExt::next, render them, apply
  the batch as one undo entry on finish, and expose an always-available Stop.
- editor_macro_run refactored into UI-thread helpers (make_run_request +
  apply_and_report) shared by the run path and tests.

fidelity-status §12: the in-app runner is now fully interactive; remaining
tail is rich-text/multi-section writes + the spreadsheet object model.
Workspace clippy -D warnings, fmt, and all gates green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Engine for the two security-critical Phase 6 surfaces (macro spec §5.6,
§6.3), fully unit-tested.

- events: recognises the well-known auto-run handler names (Document_Open,
  AutoOpen, Workbook_Open, OnLoad, …) and their phase (open/close/save).
  auto_open_handlers() filters a proc list to the on-open (T1) set.
- Auto-run is type-gated: AutoRunToken has no public constructor;
  MacroService::authorize_auto_run yields Some ONLY when the document is
  persistently trusted AND the user set auto_run_open. MacroRuntime::run_event
  requires the token, so an app cannot fire an on-open handler without passing
  that gate — "nothing fires without the flag" enforced by the type system.
- MacroRuntime::eval_udf: compute-only spreadsheet UDFs (§6.3). Runs a
  function with CapabilityBroker::for_udf (zero capabilities — not even
  DocRead — and no prompts) under a tight fuel budget; any object-model
  access, dialog, "never"-list call, error, or runaway loop returns
  UdfOutcome::Macro (the cell's #MACRO!).

T1 regression corpus (events_udf_tests): disabled / session-only / trusted-
without-flag all refuse to authorize; only trusted+flag mints a token;
revoking the flag or keep-disabled revokes it. UDF corpus: pure compute
returns the value; DocRead / MsgBox / Shell / infinite-loop / unparseable
all → #MACRO!.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
On-open handlers (Document_Open/AutoOpen/…) now fire in the word processor,
but only when the document is already trusted AND the user set the per-document
auto_run_open opt-in — the T1 exit criterion ("nothing fires without the flag").

- editor_macro_runner_ops: launch() gains an `auto` path re-checking
  MacroService::authorize_auto_run at fire time and driving the token-gated
  MacroRuntime::run_event; start_auto_run wrapper + auto_open_entries filter.
- editor_macro_runner: `auto_fire` prop fires the first on-open handler once
  on mount via use_hook.
- editor_macro_notice: a per-payload-hash use_effect (re-runs on doc load)
  authorizes then mounts the runner with auto_fire; extraction split into
  editor_macro_extract for the 300-line ceiling.
- AutoRunToken is now Copy so it can cross into the worker thread.
- fidelity-status §12: record Phase 6 (auto-run gating + UDF core); tail =
  button/control-assigned macros, class modules, Find, live UDF recalc wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds `Class <Name> … End Class` class modules (macro spec §4.2): instance
construction via `New`, fields, `Sub`/`Function`/`Property Get/Let/Set` methods,
`Me`, and implicit member access inside method bodies. Reference semantics fall
out of reusing `Value::Object` handles (Set/Is/With unchanged).

Security: a class instance is pure interpreter heap. Handles are allocated from
a high partition (USER_OBJ_BASE) and dispatched from the interpreter's own
`instances` table BEFORE the Host fallthrough, so a user class never reaches the
capability seam — it grants a script no authority it did not already have. This
is verified by running the class tests against NullHost. `New` of an unknown
(non-user) class stays an untrappable feature-refusal (external COM/ProgID, §7).

- ast: Item::Class(ClassDef { name, fields, methods }).
- parser: Class body reuses parse_item for members; also accept bare
  `Public/Private/Static name As Type` fields (no `Dim`) at module/class level.
- interp/class.rs: Instance table + construct/get/call/set/implicit dispatch and
  Me-bound method invocation; eval/call/exec route instances before the host.
- 10 class tests (New, fields, property pairs, Me, by-ref sharing, identity,
  args, refusal, 438) all green against NullHost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the Word Find/Replacement surface (macro spec §6.1) over the neutral
document body: Selection.Find / Range.Find with .Text, .Replacement.Text,
.MatchCase, .WholeWord, and .Execute (search / replace-all).

Semantics: Execute replaces every match with Replacement.Text when a replacement
has been set (Some, including "" to delete); otherwise it searches only. The
Replace:= argument is deliberately not parsed — once the host seam drops argument
names its position is unreliable, so Replacement.Text is the single predictable
replace signal (documented). MatchCase (ASCII case-fold when off) and WholeWord
are honoured via a char-based, non-overlapping matcher.

Gating: a search-only Execute gates DocRead (baseline); a replacing Execute
additionally gates DocWrite and records exactly one DocEdit::SetText, so a
find/replace run is one undo entry. Denied DocWrite is trappable (70) and records
no edit.

- exec/mod.rs: SELECTION/FIND/REPLACEMENT handles, selection/range roots,
  FindState on DocFacade.
- exec/find.rs: get-side dispatch + matcher; facade.rs: set-side params +
  Document.Range.
- 9 tests (replace-all as one batch, search-only, case sensitivity, whole-word,
  empty-replacement delete, denied-write, Range alias).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Cell formulas can now call the workbook's macro functions. An unknown function
name routes to MacroRuntime::eval_udf (compute-only, macro spec §6.3): the UDF
gets zero capabilities (not even DocRead), cannot prompt, and is fuel-bounded, so
recalculation stays pure, fast, and unpromptable. Any object-model access,
dialog, "never"-list call, error, or runaway loop yields #MACRO!.

Security note: a UDF is safe by construction, so it needs no trust grant — unlike
an executable macro (disabled by default), a UDF cannot read the document, reach
the network, or touch the filesystem, so evaluating one from any workbook cannot
cause harm.

- formula/udf.rs: UdfResolver indexes procedures by name from the preserved VBA/
  Basic payload and evaluates one via eval_udf.
- formula/mod.rs: CellValue{Num,Text} + FormulaError::Macro (#MACRO!); threads the
  resolver through evaluate_cell/evaluate_formula.
- formula/eval.rs + funcs.rs: unknown names dispatch to the UDF (numeric context);
  a whole-formula UDF may return text. eval.rs split under the 300-line ceiling.
- editor_load.rs: XlsxImport/OdsImport::run retains the payload; udf_from builds
  the resolver. editor_state/editor_inner hold it in a signal and pass it into
  the render-time evaluator.
- 6 integration tests (numeric, text, #MACRO! on doc-access / runaway, #NAME? for
  unknown, builtins not shadowed).

Limitations (documented): UDF arguments are numeric/cell-reference only (the
formula lexer has no string literals); a UDF text result is usable only as a
whole-formula value, not inside arithmetic (→ #VALUE!); eval_udf re-parses the
module per call (bounded to cells that actually use a UDF).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Word MACROBUTTON fields carried the control→macro assignment but were dropped to
inert FieldKind::Raw text on import. This models them as a typed
FieldKind::MacroButton { macro_name, display }, so the assignment is preserved
(and round-trips: the DOCX exporter re-emits `MACROBUTTON <macro> <display>`),
and the button renders its visible label in layout.

Security: modelling the assignment never implies running it — execution stays
gated by the same trust rules as any macro (disabled by default for documents the
user did not author, spec §2). The macro name is now discoverable/runnable through
the existing trust-gated Tools ▸ Macros runner.

- loki-doc-model: FieldKind::MacroButton variant.
- loki-ooxml: parse MACROBUTTON at import + re-emit on export; round-trip tests.
- loki-layout: MACROBUTTON renders its label.

Deferred (documented in fidelity-status §12): in-page click-to-run — a control
region hit-test dispatching a trust-gated run — and ActiveX/OLE form controls.
Both need the greenfield control-modeling chain (body-model control geometry +
layout hit-testing + export), out of scope for this pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
…on in fidelity-status

§12 now reflects the four tail items as landed (class modules, Find/Replacement,
compute-only spreadsheet UDFs wired into recalc, MACROBUTTON modelling) with the
honest remainders documented: in-page click-to-run + ActiveX/OLE controls, UDF
string-arg/text-in-arithmetic limits, and multi-module .cls class registration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Lays the groundwork for in-page click-to-run. A MACROBUTTON's layout run now
carries a `loki-macro:<name>` link URL, so the editor's existing hyperlink
hit-test (link_at) surfaces the click — the loki-text side then routes the
`loki-macro:` scheme to the trust-gated runner instead of a browser.

- loki-doc-model: MACRO_LINK_SCHEME const + FieldKind::macro_link() — single
  source of truth for the pseudo-URL both layout and the editor agree on.
- loki-layout: the Inline::Field walk tags a MacroButton run's link_url with the
  macro scheme (falling back to any enclosing hyperlink for non-macro fields);
  test asserts the label renders and the span carries loki-macro:RunReport.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
claude added 26 commits July 23, 2026 12:07
Add verify_signed_data to loki-macro-sig: parses a detached PKCS#7
SignedData over the macro payload and returns a total SignatureVerdict.

- verify.rs: ContentInfo -> SignedData -> first SignerInfo; checks the
  signedAttrs message-digest == digest(content) (ContentMismatch else),
  then verifies the signature over the 0x31-re-encoded signedAttrs
  (RFC 5652 §5.4 — the `cms` SetOfVec `to_der()` gives the universal
  SET OF tag, not the [0] IMPLICIT one). No signedAttrs -> verify over
  content directly. Verdict precedence: LegacyAlgorithm > CertificateExpired
  > NotPinned. Never returns ValidTrusted — trust is a caller decision
  against a user-pinned publisher store (8A.5, T10).
- verify_crypto.rs: DigestId/SigKind OID agility (SHA-2/1 + MD5;
  RSA PKCS#1v1.5 + ECDSA P-256), digesting, and the crypto check. Legacy
  digests are checked but flagged, never trust-eligible.
- verify_cert.rs: signer-cert resolution (issuer+serial / SKI), CertInfo
  extraction (CN, DNs, serial hex, validity), SHA-256 leaf thumbprint over
  the re-encoded cert DER.
- Tests build real detached CMS in-process over fresh self-signed RSA and
  P-256 certs (rcgen + RustCrypto signers): happy paths, tampered content,
  corrupt signature, expired cert, legacy SHA-1, and garbage/panic-freedom.
  Real-Office-corpus cross-validation stays a TODO(8A.3-corpus) gate.

Also drop the discarded binding in the 8A.2 fuzz target (black_box), which
had left check-suppressions red on the branch. Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Two allowlist additions from the 8A.3 work (reading rcgen's source while
building the self-signed signature fixtures). Local settings only; no code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Add verify_xmldsig to loki-macro-sig: verifies the W3C XMLDSig macro
signatures ODF stores in META-INF/macrosignatures.xml, returning the same
total SignatureVerdict as the CMS path.

- xml_c14n.rs: inclusive Canonical XML 1.0 (without comments) over a small
  namespace-resolved node tree — the transform XMLDSig signs SignedInfo
  under. Handles namespace hoisting onto the node-set apex, attribute
  sorting by (namespace URI, local name), and the c14n text/attr escaping.
  Unit-tested against hand-derived vectors.
- odf_dom.rs: a total quick-xml -> tree builder (whitespace preserved, CRLF
  normalised, predefined entities resolved; comments/PIs/decl/DOCTYPE
  dropped as c14n omits them).
- odf.rs: structural macrosignatures.xml parse — SignedInfo (with in-scope
  namespaces flattened onto the apex), references, signature value, and the
  X509 certificate — resolving no algorithms, so an unknown one still parses
  and reads as UnsupportedAlgorithm rather than vanishing.
- verify_odf.rs: for each Signature, check every package-part Reference
  digest against the real part bytes (caller resolve_part closure),
  canonicalise SignedInfo, and verify its SignatureValue — reusing the
  shared cert (verify_cert) and verdict-reason (verify.rs) layers.
- verify_crypto.rs: add EcdsaEncoding so ECDSA verifies raw P1363 r||s
  (XMLDSig) as well as DER (CMS); the CMS path passes Der unchanged.
- Tests build real macrosignatures.xml with a canonical hand-authored
  SignedInfo signed by fresh self-signed RSA / P-256 keys: happy paths,
  tampered/missing part, corrupt signature, expired cert, legacy sha1,
  unknown algorithm, unsigned, and panic-freedom. New cargo-fuzz target.
  Real LibreOffice/Word corpus interop stays a TODO(8A.4-corpus) gate;
  in-document #-fragment reference digests are authenticated by the
  SignedInfo signature but not independently re-canonicalised.

Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Add the *trust* half of the signed-macro tier to loki-macro-host (now
depending on the L2 loki-macro-sig verifier). Verification proves
authorship; trust is a separate, explicit user act (ADR-0014 §4.3/§4.5,
T10) — pinning a signer certificate's SHA-256 thumbprint.

- trust/publisher.rs: TrustedPublisherStore, a per-user profile-JSON store
  keyed by thumbprint (sibling of TrustStore, same "nothing in a document
  writes it" rule; same load/load_or_empty/save robustness). PublisherRecord
  carries the thumbprint plus display name and subject/issuer (identity kept
  only for renewal detection). pin/unpin (un-pin is the local revocation
  mechanism), contains, renewed_match.
- resolve(): the ONLY place a verified signature becomes ValidTrusted —
  upgrades a NotPinned verdict iff the signer thumbprint is pinned; flags a
  pinned-identity/new-thumbprint certificate as PublisherRenewed (re-pin
  affordance); and, as the downgrade/expiry defence, never upgrades a
  LegacyAlgorithm or CertificateExpired verdict even when pinned. All other
  verdicts pass through unchanged.
- trust/record.rs: add Provenance::TrustedPublisher { thumbprint } (hex
  serde), the provenance a document open records when a signature resolves
  to ValidTrusted.
- Unit tests cover upgrade, renewal, non-renewal (different identity), the
  never-upgrade guards, pass-through of Invalid/Unsigned, pin/contains/unpin,
  from_cert_info, and persistence round-trip / corrupt-file degradation.

Update fidelity-status §12. Trust-dialog/pin UI is 8A.7; open-path wiring
and RFC-3161 timestamps are 8A.8/8A.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The CMS verifier now honours an expired signing certificate when the
signature carries a bound RFC-3161 timestamp proving it was signed while
the certificate was valid (ADR-0014 §4.4).

- timestamp.rs: reads the id-aa-timeStampToken unsigned SignerInfo
  attribute, decodes the TimeStampToken (ContentInfo/SignedData) to its
  TSTInfo, and extracts genTime — but only after verifying the token's
  messageImprint hashes to exactly this signature's octets, so a timestamp
  cannot be transplanted from another signature.
- verify.rs: is_expired_at() treats a genTime within [notBefore, notAfter]
  as valid-at-signing, so an expired-but-timestamped signature reads
  NotPinned (eligible for the 8A.5 pin upgrade) instead of
  CertificateExpired. untrusted_reason gains the signed_time argument; the
  ODF path passes None.
- Tests build real expired-cert CMS fixtures with a bound timestamp:
  in-window rescues, out-of-window and no-timestamp stay expired, and an
  unbound (wrong-imprint) timestamp is ignored.

Deliberate limitation TODO(8A.6-tsa-anchor): the timestamp is bound and
parsed but the TSA's own signature/chain is not anchored to a trusted
timestamping root. This is safe under the leaf-thumbprint trust model —
only a pinned publisher's genuinely-signed content is ever rescued, so a
forged genTime cannot make attacker content trusted — and should be
revisited only if a chain-to-CA trust mode is added. ODF/XAdES timestamps
are a separate TODO(8A.6-xades). Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Surface macro-signature state and the "Trust this publisher" flow.

loki-macro-host:
- MacroService now owns a TrustedPublisherStore alongside the TrustStore
  (a sibling profile file), records each open document's verdict via
  set_signature, and resolves it against the pins on read so a fresh pin
  flips the shown trust live. New pin_publisher / unpin_publisher /
  trusted_publishers / is_publisher_trusted.
- SignatureSummary / SignatureStatus view model folds a SignatureVerdict
  into the states the UI distinguishes (Unsigned / Invalid / Untrusted /
  Legacy / Expired / Renewed / Trusted); can_pin is true only for
  Untrusted and Renewed. Unit-tested: pinning flips a NotPinned summary to
  Trusted; expired/legacy never upgrade even when pinned.

loki-i18n: macros-sig-* strings (signature states, Trust-this-publisher
confirm, management list, revocation note).

loki-text: MacroSignatureSection renders inside the Document Security
panel — 🔏 signed-by + issuer + thumbprint, a "Trust this publisher…"
action behind an anti-spoof-framed inline confirm, the trusted note, and
the pinned-publisher management list with per-row Remove.

Open-path wiring (verify on open → set_signature → enabled-at-open) and
the editor "editing removes the signature" warning are 8A.8. Update
fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
…servation

Wire the verifier to a real opened document, end to end for ODF.

loki-odf: preserve META-INF/macrosignatures.xml / documentsignatures.xml
alongside the Basic/Scripts parts (they are not manifest-declared, so they
are collected out-of-band and, on export, written to the ZIP but kept out
of the manifest). This gives the ODF XMLDSig verifier real input and fixes
a round-trip gap (signatures were silently dropped on save).

loki-macro-host:
- verify::verify_payload(&MacroPayload) -> SignatureVerdict: locates the
  signature in a preserved payload and feeds the verifier the exact source
  bytes. ODF is verified end-to-end (macrosignatures.xml XMLDSig checked
  against the other preserved parts via a resolver closure); VBA is deferred
  (TODO(8A.8-vba-content): needs the MS-OVBA content hash) and reads Unsigned
  rather than a misleading Invalid.
- MacroService::verify_and_record — the open-time entry point — and
  is_enabled now also returns true for a pinned trusted publisher
  (enabled at open, ADR-0014 §4.5; sensitive capabilities still prompt and
  auto-run still needs its own opt-in).
- End-to-end tests build a real signed ODF macrosignatures.xml: verify →
  ValidUntrusted, tamper → Invalid, unsigned → Unsigned, VBA → Unsigned,
  and pin → is_enabled at open.

loki-text open-path wiring and the editor "editing removes the signature"
warning are part 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Wire signature verification into the loki-text document-open flow,
completing Track A.

- use_verify_signature_effect: on document load, verify the preserved macro
  signature once per payload and record the verdict in the MacroService
  (before the auto-run gate), so a trusted-publisher document is recognised
  at open (ADR-0014 §4.5).
- The macros infobar reflects publisher trust: a signed-by-trusted-publisher
  document reads as enabled at open ("Macros enabled — signed by a trusted
  publisher") without a per-document decision. Sensitive capabilities still
  prompt; auto-run still needs its own opt-in.
- The macro editor shows an "editing removes the publisher's signature"
  warning atop a signed project (ADR-0014 §4.6).
- macros-sig-enabled-publisher / macros-sig-edit-warning strings.

Update fidelity-status §12: Track A complete; residual gates
(VBA content hash, corpus validation, TSA anchoring, XAdES timestamps)
documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Four fixes from the Track A self-review:

1. Signature coverage (security). verify_xmldsig now takes a require_covered
   list and rejects (ContentMismatch) any signature that does not reference
   every part the runner will execute. verify_payload passes all non-empty
   Basic/Scripts file parts. Previously a signature that referenced zero (or
   only a subset of) the macro modules verified as valid, so unreferenced —
   hence unsigned — modules ran under a "valid"/trusted signature.

2. MACROBUTTON click gate. use_click_dispatch_effect now gates on
   svc.is_enabled() rather than decision_for().is_enabled(), so a
   trusted-publisher document (enabled at open, no per-document record) runs
   a clicked macro instead of re-prompting — matching the infobar.

3. Editing drops the signature. build_edited_payload removes the
   META-INF/*signatures.xml parts, so an edited document is saved cleanly
   *unsigned* instead of carrying a stale signature that other apps flag as
   tampered — making the editor warning truthful (ADR-0014 §4.6).

4. Remove a dead empty `else if` branch in the signature section.

New tests: signature-not-covering-a-required-part → Invalid (verifier),
unreferenced-module → Invalid (host open path), and editing-drops-the-
signature (editor ops). Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Start Track B (ADR-0015). The Network capability becomes reachable only
through a bounded, origin-scoped, session-max path — never a wholesale
"network on" grant.

- net.rs: NetworkPolicy (per-run enabled flag + allowed origins) and
  MACRO_NET_COMPILED (the cfg!(feature = "macro-net") constant). Off by
  default; the app enables only when the build feature AND the runtime
  setting are both on (ADR-0015 §8 decision 1). Grants never persist to
  disk (§4.2, the T4 exfiltration edge).
- broker.rs: CapabilityBroker gains a network policy (with_network) and
  evaluate_network / apply_network_prompt. Network is Refused when disabled,
  Denied in a UDF, Granted only for an already-allowed origin, else Prompt —
  per distinct origin, no wildcards. The generic evaluate(Capability::Network)
  stays Refused (a bare network switch is never a grant).
- Cargo.toml: off-by-default `macro-net` feature (excluded from the iOS
  build per §8 decision 5).
- Tests (net + broker), passing with the feature both off and on.

Deferred to 8B.2–8B.6: the HttpGet shim, the reqwest HTTPS-only backend,
bounds/Stop, the per-host prompt + composition warning, and the
always-refused headless/server backend. Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The object-model network verb (ADR-0015 §4.1). Read-only GET first;
HttpPost stays deferred (§8 decision 3).

- http.rs: HttpRequest / HttpResponse / HttpError plus origin_of (the
  grant unit) — https-only, userinfo rejected (spoofing / ambient-cred
  smuggling). HttpResponse exposes body_as_string (lossy; never parsed
  into a privileged format, T9) and case-insensitive header lookup.
- exec: MacroBackend gains prompt_network + http_get, both defaulting to
  refused so any backend that does not opt in (UDF, headless, tests) has
  no network. ExecutionHost.http_get validates the URL, gates the origin
  through the broker's per-origin path (gate_network, prompting via the
  seam), performs the fetch, and returns an HttpResponse object handle;
  the macro reads .Status / .Text / .Header. Split into exec/network.rs
  for the 300-line ceiling.
- RunRequest.with_network threads the origin-scoped policy into the run.
- End-to-end tests: fetch reads back 200/"pong"; a disabled policy refuses
  untrappably before the backend; a denied origin never reaches the
  backend; a non-https URL is rejected before prompting. Passing with the
  macro-net feature both off and on.

Update fidelity-status §12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Post-rebase reconciliation after replaying the macro-scripting branch
onto the new main tip (DOCX repair + layout/ooxml/odf work):

- scripts/suppressions-baseline.txt: regenerated with
  check-suppressions.py --update so the ratchet reflects the merged
  tree (main's + macro suppressions combined); gate passes.
- loki-layout/src/resolve_inlines.rs: main brought this file to exactly
  the 300-line ceiling; the MACROBUTTON field-display arm pushed it to
  303. Collapsed the redundant `String::new()` match arms (all already
  covered by the existing `_` wildcard) into that wildcard — behaviour
  unchanged, file back to 295.
- loki-text/src/routes/editor/editor_inner.rs: dropped one blank line
  so the macro wiring (macro_run_request signal + MacroNoticeBar mount)
  keeps the file at its 800-line baseline instead of growing to 801.

All CI gates green; cargo check --workspace, clippy on the touched +
macro crates, and the affected layout/hit-test/macro-host suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Implements the real macro `Network` transport behind the off-by-default
`macro-net` build feature (ADR-0015 §4.1, §4.3).

- net_fetch.rs (compiled only under `macro-net`): `NetFetcher`, a blocking
  reqwest/rustls client that performs a bounded, read-only HTTPS GET. The
  client follows **no** redirects itself (`redirect(Policy::none())`) so each
  hop is re-gated in-process against the session origin allow-list; it sets
  `https_only`, carries no cookie store and no default/proxy auth (no ambient
  credentials leave — T4), and a 30 s timeout.
- net_policy.rs (always compiled, reqwest-free so it unit-tests without the
  feature): the header deny-list (framing/hop-by-hop + ambient-credential
  `Cookie`/proxy-auth stripped; an author-set `Authorization` is kept — it is
  the author's own explicit credential), relative-redirect resolution +
  per-hop origin re-check (`redirect_next`), and the hop/size bounds. A
  redirect to a not-yet-granted origin is refused (trappable), never silently
  followed. 12 unit tests.
- MacroBackend::http_get now receives the granted origins so a backend can
  bound its redirect following; ExecutionHost::http_get snapshots them from
  the broker after gating (new CapabilityBroker::network_origins).

Both feature configs pass clippy -D warnings and tests. A true streaming
size-cap and Stop-cancels-the-request are the 8B.4 follow-ups (TODO(8B.4));
wiring the app BridgeBackend into NetFetcher lands with the per-host prompt
in 8B.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Turns the read-then-measure size check into a true bound and wires the
shared Stop cancel flag into the fetch (ADR-0015 §4.1).

- net_policy::read_body_capped (pure, reqwest-free, unit-tested): streams a
  response body reading at most `cap + 1` bytes, so an undeclared or endless
  over-cap body is bounded without a large allocation, and checks the shared
  cancel flag between 16 KiB chunks so Stop lands promptly.
- NetFetcher::fetch now takes the cancel flag, checks it before each redirect
  hop, and reads the terminal body through read_body_capped (keeping the
  cheap content-length early-reject). New HttpError::Cancelled maps to a
  trappable interrupt.
- The 30 s whole-request timeout still bounds how long Stop can take to land
  while blocked in connect/TLS; per-run-configurable timeout is TODO(8B.4-config).

4 added unit tests (under/at/over cap, cancel-before-read). Both feature
configs pass clippy -D warnings and tests; workspace builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the app surface for the macro Network capability (ADR-0015 §4.2, §4.6).

- appthere_ui::AtNetworkPrompt: a per-host prompt in the anti-spoof
  MacroDialogFrame. Shows the destination origin verbatim (in its own field
  so a look-alike host is legible) and offers only Deny / Allow-once /
  Allow-session — no "always for this document", since network grants are
  session-max. Always carries the composition warning (§4.6), which applies
  unconditionally because DocRead is baseline (a macro can always read the
  document). TODO(8B.5-homograph) for punycode/homograph display.
- loki-text BridgeBackend: implements prompt_network (forwards a new
  UiRequest::Network(origin) through the existing worker↔UI bridge, blocking
  for the answer) and, under the macro-net feature, http_get (lazily builds a
  NetFetcher and fetches on the worker thread with the shared Stop cancel
  flag). New macros-net-* i18n strings. loki-text gains a macro-net feature
  that enables loki-macro-host/macro-net.
- answer_prompt skips capability-grant recording for a Network request (the
  broker records the session origin during the run); TODO(8B.5-session-origins)
  for cross-run session-origin memory.

Threaded test: HttpGet to a new origin raises the per-origin prompt with the
exact origin and a Deny traps the call (holds under either macro-net config).
Both feature configs pass clippy -D warnings and tests; gates green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Completes Track B by documenting and testing the always-refused posture
for headless/server/UDF contexts (ADR-0015 §8 decision 4/5).

Two guarantees keep macro network access off everywhere except an
opted-in interactive client:

1. Compile-time: no server or headless crate links loki-macro-host at all
   (it pulls the loki-basic interpreter, which check-loki-basic-pure.py
   forbids server/headless crates from linking), so the reqwest/rustls
   transport is not even present in those binaries.
2. Run-time: every non-interactive run uses DenyBackend (whose http_get
   refuses), and a spreadsheet UDF is additionally compute-only (network
   disabled, cannot prompt) — HttpGet there yields #MACRO!.

tests/network_refusal_tests.rs covers the run-time guarantees in both
feature configs; the compile-time one is enforced by the purity gate in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the read half of picker-mediated file access to the macro object
model (macro spec §5.3, threat T3). There is no path-addressed file API —
a macro can only ever touch a file the user hands it through the OS picker.

- file.rs: PickedFile (path + bytes, run-scoped, read-only) and FileFilter
  with a tolerant parser (*.txt, *.a;*.b, "Text Files (*.txt),*.txt",
  txt,csv → bare lower-cased extensions). Unit-tested.
- MacroBackend::read_file(filter) -> Option<PickedFile> seam (default None,
  so UDF/headless/tests read no files — the pick is the app's job).
- exec/file.rs: Application.OpenFileForReading([filter]) gates the FileRead
  capability, raises the picker via the backend, and returns a run-scoped
  handle. A cancelled/empty pick is a trappable error (75). Handle members
  .Path / .Text (.ReadAll) / .Length read the picked bytes with no further
  capability (the gate + pick were the gated acts). FILE_HANDLE_BASE is a
  distinct handle range above HTTP_RESPONSE_BASE (checked highest-first).

End-to-end tested with a mock picker (tests/file_tests.rs): reads into the
doc, denied FileRead never raises the picker, a cancelled pick is trappable
(directly and via On Error), and .Path/.Length read back. Both feature
configs pass clippy -D warnings and tests; gates green; workspace builds.

Still deferred: the loki-text app-side picker bridge (async OS picker over
the worker↔UI bridge) and FileWrite (picker-chosen save target).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Adds the write half of picker-mediated file access (macro spec §5.3,
threat T3). Symmetric with FileRead: no path-addressed API — a macro can
only write to a target the user picks through the OS save picker.

- Application.OpenFileForWriting([filter]) gates the FileWrite capability,
  raises the save picker via a new MacroBackend::pick_write_target seam
  (the chosen target is the second consent), and returns a run-scoped
  write handle. A cancelled pick is a trappable error.
- Handle members: .Write / .WriteLine (.Print) buffer text; .Close flushes
  the buffer to the picked path through MacroBackend::write_file. A failed
  flush is a trappable error (new FileWriteError); an unclosed handle
  writes nothing (explicit-close contract). Both backend seams default to
  no-op/refuse, so UDF/headless/tests write no files.
- WRITE_FILE_BASE is a third, highest object-handle range (dispatched
  first). The DocFacade struct + handle tables moved to exec/doc_facade.rs
  to keep exec/mod.rs under the 300-line ceiling.

tests/file_tests.rs now 10 cases (read + write): buffered Write/WriteLine
flush on Close, denied FileWrite never raises the picker, cancelled save
pick is trappable, a write failure is trappable, and an unclosed handle is
not flushed. Both feature configs pass clippy -D warnings and tests; gates
green; workspace builds.

Still deferred: the loki-text app-side picker bridge (the read + write
pickers are already async there, so it is wiring, not new infra).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Completes Phase 7B — macro file access now works end-to-end in the word
processor. The macro-host core (OpenFileForReading/OpenFileForWriting +
the read_file/pick_write_target/write_file seams) is unchanged; this wires
the real OS picker + platform file I/O into it.

- editor_macro_bridge: three new UiRequest variants (PickReadFile,
  PickWriteTarget, WriteFile) + matching UiReply (ReadFile, WritePath,
  WriteResult). BridgeBackend implements the three MacroBackend file seams
  by posting over the existing worker↔UI bridge and blocking for the reply.
- editor_macro_file_pick (new): the runner's drain loop services a file
  request inline — it drives the async loki_file_access::FilePicker and
  does byte I/O through the platform FileAccessToken (open_read/open_write),
  both on the UI thread (Android content-URI tokens used on the right
  thread; no path-addressed std::fs). FileFilter → picker MIME types is
  best-effort/advisory; the user's pick is the authority.
- The drain loop hands file requests to try_handle_file_request; everything
  else still renders as a prompt component. from_request/answer_prompt gain
  inert arms for the (never-rendered) file requests.

Threaded bridge round-trip tests cover both directions (read a canned file
into the doc; buffered write reaches write_file with the exact path+bytes
on Close). Both feature configs pass clippy -D warnings and tests; gates
green; workspace builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Makes the macro Network capability reachable end-to-end (ADR-0015 §8). The
capability was gated behind BOTH the macro-net build feature and an
"off-by-default runtime setting" — the runtime half was unbuilt, so
network could never actually turn on. This adds it as a per-document
opt-in, mirroring the existing auto_run_open precedent.

- TrustRecord.allow_network (serde-defaulted for back-compat) +
  MacroService::network_enabled / set_allow_network (requires a persistent
  trust record; per-origin prompts still gate every request).
  DocumentSecurity snapshot carries allow_network.
- loki-text make_run_request enables the run's NetworkPolicy only when
  MACRO_NET_COMPILED && svc.network_enabled(payload); otherwise it stays
  disabled and every HttpGet is an untrappable refusal.
- Document Security panel: an "Allow network access" checkbox (sibling of
  auto-run) shown only when the feature is compiled and the document is
  trusted. macros-security-network* i18n.
- The per-document opt-in cluster (auto-run + network) moved to
  service/opt_in.rs to keep service/mod.rs under the 300-line ceiling.

Round-trip tested (network_enabled_defaults_off_and_requires_record: off by
default, no-op without a record, sticks with trust, independent of
auto-run). Both feature configs pass clippy -D warnings and tests; gates
green; workspace builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
The four open Track A gates (8A.3/8A.4-corpus, 8A.8-vba-content,
8A.6-tsa-anchor/xades) are all blocked on the same thing: a corpus of real
third-party-signed documents. Every current signature test builds its own
fixture in-process (fresh rcgen keys, our own canonicaliser producing the
signed octets), which proves self-consistency but cannot prove agreement
with Word or LibreOffice — which is the entire point of a verifier.

docs/macro-signature-corpus.md records what is needed to close them:

- The sample matrix (VBA V-1..V-7, ODF O-1..O-6, timestamps T-1..T-3),
  with how to produce each in Word/LibreOffice, including the negatives
  (edited-after-signing must read Invalid) that prove the content hash is
  load-bearing rather than vacuous.
- The commit constraint: a signer's certificate carries real identity and
  a signature cannot be redacted, so self-produced samples (fictitious
  subject) are committed under assets/ per the loki-acid precedent while
  vendor-signed ones stay local behind LOKI_MACRO_CORPUS.
- The per-gate unblock order, including the ADR-0014 §6 decision point
  (fall back to rasn-cms if a real SignedData quirk defeats RustCrypto).
- An explicit warning not to implement these from the spec alone: an
  unvalidated content-hash normalisation reports every real signed
  document as Invalid, which is worse than today's honest Unsigned.

Linked from the fidelity-status §13 residual-gates note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Seven findings from the review of 16be9bd..HEAD (Phase 8B network + Phase
7B picker-mediated file access).

Correctness:

- Object-handle collision (doc_facade): the handle bases are 0x1000 apart
  and the tables were unbounded, so the 4097th HttpGet response minted
  FILE_HANDLE_BASE and facade::get_member — which dispatches
  highest-base-first — read it back as a picked file. Tables are now capped
  at MAX_OBJECTS_PER_KIND, and the disjointness is enforced by a
  `const _: () = assert!(..)` so raising the cap past the spacing fails the
  build rather than silently corrupting dispatch.
- Unbounded retention (doc_facade): each response/file is individually
  capped but the count was not, so a fetch loop over one granted origin
  could exhaust memory. Added a run-wide MAX_RETAINED_BYTES budget charged
  by responses, picked files, and `.Write` buffers; exhaustion is a
  trappable error.
- Failed `.Close` was not retryable (exec/file): `closed` was set before the
  write, so after a failed flush a second `.Close` returned success without
  writing and an On Error-retrying macro believed the file was saved. Only a
  successful flush closes the handle now.
- Origin/redirect normalization mismatch (http): `origin_of` hand-parsed the
  authority while redirect targets resolved through `url::Url`, which strips
  the scheme-default port and punycodes IDN hosts — so a grant taken on an
  explicit `:443` URL did not match the origin of its own same-host
  redirect. `origin_of` now uses the same parser the client uses; an origin
  check that parses differently from the code issuing the request is how
  origin checks get bypassed.
- Opaque token leaked as a path (file_pick + file): `.Path`/`.Name` returned
  `FileAccessToken::serialize()` — URL-safe base64 of the token's JSON, on
  Android a content-URI permission grant. Split into a macro-visible
  `display_name` and an opaque `handle` (new `WriteTarget`; `PickedFile.path`
  renamed to `display_name` so the contract is self-documenting).
- Stale network opt-in (service/opt_in): `network_enabled` lacked the
  `decision.is_enabled()` check its sibling `authorize_auto_run` has, and
  `upsert_decision` preserves the flag across a decision change — so a
  document later set to "Keep disabled" kept a live opt-in the panel gives
  no way to revoke.

Docs:

- net_policy: the TODO(8B.4) still described the read-then-measure cap that
  8B.4 replaced with the streaming read_body_capped in the same file.

Regression tests: default-port + IDN origin equality, failed-close retries
(and successful close stays idempotent), disabled document drops the network
opt-in, `.Path` is the display name. Both feature configs pass clippy
-D warnings and tests; all gates green; workspace builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
CI's lint job failed on `loki-macro-sig/src/verify.rs:155`:
`.map(f).unwrap_or(0)` on the `Result` from `duration_since` should be
`.map_or(0, f)`. Semantically identical; clippy's own suggested rewrite.

Why local checks missed it: CI installs `dtolnay/rust-toolchain@stable` and
the repo pins no `rust-toolchain.toml`, so CI floats on latest stable
(1.97.1) while this sandbox had 1.94.1. `map_unwrap_or` was extended to
`Result` receivers in between — the `Option` form this workspace already
uses elsewhere was clean under both, so nothing fired locally. CI also runs
`--workspace --all-features`, broader than a per-crate invocation.

The local toolchain is now 1.97.1 (clippy 0.1.97), matching CI, and a full
`cargo clippy --workspace --all-features -- -D warnings -D
clippy::unwrap_used -D clippy::expect_used` is running to catch anything
CI's fail-fast hid behind this first error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
CI fails fast, so these were hidden behind the first error.

- loki-basic/src/parser/mod.rs: `peek()` used `.expect("token buffer always
  contains Eof")` — a genuine CLAUDE.md violation (no `.expect()` in library
  code) that CI catches via its extra `-D clippy::expect_used`, which my
  per-crate runs omitted. Made total instead of allow-listed: the cursor
  falls back to the trailing `Eof` token (preserving the real end-of-source
  span, so diagnostics are unchanged), with a `static EOF_TOKEN` covering
  only the empty-buffer case the lexer never produces.
- loki-macro-host/src/service/summary.rs: `sort_by(|a, b| b.x.cmp(&a.x))` →
  `sort_by_key(|r| Reverse(r.last_used))`. Another 1.97 lint; also states
  "newest first" more directly.

Local toolchain is now at CI parity (1.97.1) and a full
`cargo clippy --workspace --all-features` with CI's extra deny flags is
running to confirm nothing remains downstream — the earlier run halted at
loki-macro-host before reaching loki-text and the other leaf crates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
loki-text/src/routes/editor/editor_macro_runner.rs: the live-prompt block
guarded with `if pending.read().is_some()` and then re-read the signal with
`.expect("pending is Some")` — an unreachable panic, but still a panicking
accessor in library code (CLAUDE.md) and caught by CI's extra
`-D clippy::expect_used`.

Replaced the guard+expect pair with a single `if let`. The owned view is
bound in its own `let` statement so the signal borrow is definitively
released before the body runs — the answer callback writes the same signal,
and relying on edition-2024 `if let` temporary-drop rules for that would be
a fragile way to avoid a runtime deadlock.

Verified at CI parity (toolchain 1.97.1): the full
`cargo clippy --workspace --all-features -- -D warnings
-D clippy::unwrap_used -D clippy::expect_used` now exits 0, as do
`cargo fmt --all --check` and all nine gate scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
Root-cause fix for the four lint failures on this PR. CI installed
`dtolnay/rust-toolchain@stable` — floating on whatever stable is current
(1.97.1) — while this sandbox sat on 1.94.1. Clippy widens lints between
releases, so CI failed on lints a local `cargo clippy` could not possibly
report: `map_unwrap_or` gained `Result` coverage, and `sort_by_key` fired on
a `sort_by` that 1.94 accepted. That drift is only discoverable by pushing,
which turns it into a slow fail-push-repeat loop.

- rust-toolchain.toml pins 1.97.1 (the version CI already resolved to, so
  this locks in the currently-green state rather than moving anything) plus
  the rustfmt/clippy components. rustup honours it for every invocation in
  the repo, so local and CI now run the same compiler and the same lints.
- .github/workflows/rust.yml names the same version in both jobs instead of
  `@stable`. The file is the real pin — cargo honours it regardless — so
  this just avoids installing a second, unused toolchain.

Also fixes the *other* half of the gap, which the pin alone would not have
caught: CLAUDE.md told contributors to run `cargo clippy --workspace --
-D warnings`, but CI runs `--all-features` with two extra deny flags
(`-D clippy::unwrap_used -D clippy::expect_used`). Running the documented
command was therefore insufficient by construction, and it is why two
`.expect()` calls in library code — a stated CLAUDE.md prohibition — reached
CI. The doc now carries CI's exact command and notes that clippy.toml
exempts test code.

Verified: rustup reports 1.97.1 "overridden by rust-toolchain.toml"; cargo
and clippy resolve to the pinned versions; the workflow YAML parses and both
jobs reference the pinned toolchain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQSgJrDikHzN5tCqVzsfgB
@kevincarlson
kevincarlson merged commit 72f997c into main Jul 24, 2026
2 checks passed
@kevincarlson kevincarlson self-assigned this Jul 24, 2026
@kevincarlson
kevincarlson deleted the claude/safe-macro-implementation-circn5 branch July 24, 2026 23:47
@AppThere AppThere locked as resolved and limited conversation to collaborators Jul 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants