Attribute a clip by the text it handed Obsidian - #21
Merged
Conversation
The source-URL check tells two clips of different pages apart, but not two clips of the SAME page racing from two Gullet processes: same source, and the claim maps are per-process. tab_clip now returns contentHash — SHA-256 of exactly what it handed over — and a note that does not hash to it was written by some other clip. That identifies the invocation rather than the destination, so it needs no shared state and no marker in the user's notes. Measured before built, in both clip modes and both of the user's real vaults as well as a bare one, at 238 B and 50 kB: Obsidian writes back byte-identical text — no trailing newline added, no line endings rewritten, no truncation. That measurement is what makes a mismatch trustworthy enough to disqualify a note, and if it ever stops holding the symptom is every clip reporting mismatched, which is a verdict of its own precisely so the message can name that possibility. Also from the live run: Obsidian creates a note and fills it a beat later, and an empty file has no parseable source, so the fallback read it as ours. An empty note now never vouches for a clip. The hash is never required — an older extension sends none and attribution falls back to source. The extension hashes with WebCrypto and Gullet with Bun's hasher, so tests/clip-source.test.ts pins them together; nothing else would catch an encoding or digest change. ClipVerifier takes an evidence object rather than four positional arguments, which is the simplification pass deferred from #19. Verified: bun run check (456 tests).
The content fingerprint was taken over the LF text the extension composed, but in the default clipboard mode that text reaches Obsidian through the OS clipboard, and Windows carries plain text as CF_UNICODETEXT with CRLF. The note on disk therefore has endings the extension never produced, so every landed clip on Windows would hash differently, report `mismatched`, leave the requested close undone, and send the agent back to re-clip a page Obsidian had already filed. Both hashers now fold CRLF to LF first. Line endings are transport, not content; the only thing given up is telling apart two clips that differ in nothing else, and there is no such pair. The cross-boundary test was proving less than it claimed: it hashed with its own WebCrypto copy, so it only ever showed that Bun agrees with the test. It imports the real extension helper now — which is why that helper moves out of bridge-methods.ts, whose import graph reaches `browser`, into its own pure module — and covers both the CRLF route and a pair that must still hash apart. `tab_clip`'s MCP description still told agents a verified clip is no proof of the extraction and that concurrent same-URL sessions can share evidence. With a contentHash present that has not been true since c6a6a70; it now splits the guarantee by whether the result carries one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tab_clipaccepts avaultoverride, and until now nothing could tell a caller that the name was wrong. The handoff is anobsidian://URL given to the OS: an unrecognised vault fails inside Obsidian, where neither end of the bridge can observe it, and the call still reports success.Gullet now checks an explicit
vaultname against Obsidian's localobsidian.jsonregistry before forwarding it. A readable, understood registry that does not contain the name produces abad-requestlisting the vaults that registry knows.Deliberately a soft check
The registry is undocumented and incomplete, and its location varies outside a standard install. So absent, unreadable, malformed, unfamiliar, or empty (
vaults: {}) all preserve the old pass-through behavior — only a registry that positively contradicts the name refuses the call. An empty registry proves nothing about a name the user read off their own vault switcher, so it belongs with the other soft cases rather than producing a rejection listing "(none)".The error describes known vaults rather than claiming to enumerate every vault on disk. The extension still cannot validate the destination.
Parsed contents are cached by modification time and size (one
stat()per call), so a vault added while Gullet is running appears on the next changed-file check.Notes
knownObsidianVaultsis optional onToolContext, matchingrivalHubs?— it can refuse a call it can prove wrong, but an absent lookup never changes one, so construction sites need no stub.parseVaultOverrideis exported so the vault check reads only the vault field. It previously ran the wholeparseTabClipParams, meaning Gullet re-validatedtabIdandclose— which the extension owns — and reported those errors from a function named for the vault.vaultWarningForpath-vs-name check is unchanged.Verification
bun run typecheck,bun test(398 pass),bun run format:check,bun run lint:js.No manifest or permission changes. No UI changes.
Live verification (added after opening)
Done against a plain Firefox 134 carrying this build, attached to the same hub as my
real Zen — the hub serves both connections at once, so nothing about my session was
disturbed. The verifier ran in freshly spawned Gullet processes built from this branch.
reported
contentHash: cb7e769…, and re-hashing the file Obsidian wrote gave thesame digest. That is WebCrypto in the browser agreeing with
Bun.CryptoHasheron thefilesystem, through the OS clipboard and the
obsidian://handoff — the one link theunit tests cannot reach.
code, only the expected digest altered:
landed→mismatched. With no evidence atall the same note still verifies
landed, which is the pre-hash behaviour and showsthe hash is what does the discriminating.
the same URL simultaneously: both
landed, on distinct notes, and each reportedhash matched a different file (
21f2810f…→Crocodile 2.md,f1746a68…→Crocodile 3.md). Before this branch there was nothing to tell those two apart.Incidentally: two processes clipping the same tab at once left one clip fine and the
other
extract-failed— the same family as #20, and not something this branch changes.Also carries a one-line
.gitignorefix: dev browser profiles were ignored three-by-name, so the.dev-profile-livethis testing created was untracked but not ignored — and those profiles hold the bridge token in extension storage. Now.dev-profile*/.