feat: 3.11.0 - fde vault, a derived and disposable Obsidian view of the fieldbook - #49
Conversation
Obsidian ignores dot-paths, so ~/fde-engagements as a vault shows nothing. fde vault generates a portfolio vault from .fde/ instead: frontmatter and [[wikilinks]] in the generated copy only, rebuilt from scratch each run, gitignored, never parsed back. --redacted drops the political layer for a shared screen. Refuses $HOME, the engagements root, .fde/, symlinks, and any directory it did not write. Co-Authored-By: Subash Natarajan <suboss87@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
…k, and cannot be aimed through a symlink Three defects in the sponsor-facing build: - stripInternalTokens() collapsed all whitespace runs, and --redacted feeds it whole section bodies, so every page an FDE would screen-share became run-on text with broken tables. It now closes up only the space the removed token occupied. - next action, brief and reality were rendered raw on the hub and portfolio pages, so a [@owner] or [signal:red] typed there reached the sponsor view. - resolveVaultOut() judged containment on the textual path, so a symlinked parent (ln -s ~/fde-engagements /tmp/l; --out /tmp/l/v) aimed the output inside the engagements root. Containment is now judged on the real path, while the symlink refusal still reads the path as given.
|
Fixed in 9e728ac — real bug, and it was the worst kind: it only showed up on the pages an FDE would put on a sponsor's screen. I went slightly narrower than the suggested .replace(/[^\S\n]*\[signal:(red|amber|green)\]/gi, '')
.replace(/[^\S\n]*\[@[^\]]+\]/g, '')
.trim()Regression test asserts every heading in the source Two more from the same reading, in the same commit: |
Runtime verification —
|
| sweep | needles + fragments |
|---|---|
| full tree (21 files) | 0 |
--redacted tree (15 files) |
0 |
| both trees after all 14 verbs ran | 0 |
| 14 CLI verbs, 98 KB dashboard, 3 hooks | 0 |
An unbalanced <private> opener seals to EOF. A private table cell and ledger row render (private - redacted) in place, keeping the row structure. The only raw <private>/[signal:x] literals anywhere are in the generated README.md, which documents what it strips.
The whitespace fix is load-bearing (same fixture, pre-fix vs fixed)
| redacted page | pre-fix lines | fixed | pre-fix table rows | fixed | pre-fix max line | fixed |
|---|---|---|---|---|---|---|
Risks.md |
21 | 31 | 2 | 4 | 140 | 93 |
Delivery.md |
23 | 54 | 6 | 8 | 363 | 109 |
Terrain.md |
25 | 43 | 3 | 5 | 175 | 127 |
Pre-fix a sponsor saw # Risk register | Risk | Status | Owner | Mitigation | glued into one line. Fixed: all five section pages have identical line counts to full mode, equal table rows and headings, and a line-by-line diff whose only differences are the removed tokens. The narrower [^\S\n]* over [^\S\n]{2,} is vindicated — a markdown hard break (two trailing spaces) survives.
Token sinks: full mode has 8 [signal:] + 5 [@] live; redacted has 0 across all six sinks (section body, timeline, hub Next action/Brief/Reality, Portfolio column). At 50 engagements, 250 live tokens → 0.
Containment fix: a symlinked parent no longer smuggles output into the fieldbook
== PARENT 8a980b5 == exit=0 vault → /tmp/.../link-to-root/newvault
*** OUTPUT LANDED INSIDE THE ENGAGEMENTS ROOT
*** files written inside the root: 12
== FIXED 9e728ac == exit=1 refused: ... would contain or sit inside your engagements root
new files appearing under the root: 0
21-case refusal matrix — root, parent of root (nested root so it is distinct from $HOME), child, $HOME, /, .fde component, non-empty unstamped dir, file, fifo, /dev/null, missing value, plus 3-hop symlink chain, link→$HOME, link→.fde, dangling link — all exit 1, target byte-identical, engagements root byte-identical after the whole matrix. Allowed-by-intent cases (relative, ~/ expanded, ~foo literal, empty dir, stamped dir, --out twice) all land exactly where expected.
Never authoritative · rebuild · hostile data · packaging · YAML
Never authoritative: manifest of 111 source files byte-identical after all four modes; .fde/ gained 0 wikilinks and 0 frontmatter; an fs tracer across 11 verbs shows 0 reads inside the vault (the writer only lstats its own output for the stamp gate); deleting the vault changes no exit code.
Rebuild: two runs byte-identical; hand-typed files vanish; a removed engagement and an emptied page disappear with 0 dangling links; 154-file redacted rebuild byte-identical at 50 engagements.
Hostile data: 15 hostile engagement names + stakeholder names ../../escape, .., a/b, /etc/passwd → traversal canary never created, 0 files outside the vault, 270 wikilinks 0 dangling. fifo/symlink/dir/unreadable/5 MB/binary/CRLF/malformed: no hang, no stack trace, symlink target body leaked 0x.
Scale: 50 engagements → 255 pages in 67 ms.
Packaging: package/bin/lib/vault.js ships; tarball npm i -g into a clean prefix + clean HOME runs vault from outside any checkout with stamp "version": "3.11.0"; install.js deploys lib/vault.js and the deployed CLI runs it; npx --package=<tgz> fde vault works. The registry still publishes 3.10.2, so that path is untested.
YAML: 255 frontmatter blocks parsed with PyYAML 5.4.1, 0 errors, 0 injection under hostile values (" \ : #, emoji, braces, leading -/>).
Three intended behaviours worth a second look
- A dot-prefixed engagement dir is silently invisible to every verb (pre-existing — identical at
c960338). It is also the defence that makes an engagement named..unreachable, sincesafeTitlestrips/ [ ] | # ^but not dots. All three escape routes are closed; worth a comment insafeTitleso a future refactor cannot reopen it. FDEOPS_ENGAGEMENT=/etc fde vault --currentexits 0 with an emptyuntitledengagement instead of refusing (pre-existing). No system content leaks —root:x:0:0,nameserver,PATH=all 0 hits.- A redacted Value-ledger row still names its acceptor (
| (private - redacted) ledger row | 4h/wk saved | Dana |) because section bodies are shown as written — by design, but a sponsor-facing page naming an internal acceptor may surprise users.
Summary
Obsidian skips any path starting with
., so opening~/fde-engagementsas a vault shows nothing - every client's record lives inside.fde/.fde vaultanswers that by generating a vault instead of pointing at one:New verb, no change to the memory model:
The design rule that keeps your "nothing new to maintain" constraint intact: frontmatter and
[[wikilinks]]exist only in the generated copy. Authoritative.fde/files stay plain markdown a client can read, every existing parser is untouched, and the vault is deleted and rebuilt on every run - so a note typed into it is lost, and nothing in it can ever become a second source of truth. A test asserts both halves (the source file starts with#, not---; a hand-typedscratch.mdin the vault does not survive a rebuild).What the vault gives an FDE that no existing surface does: one window over all clients, cross-client search, graph view from sponsor → decision → what shipped, and a
Questionspage that answers "which promise has nobody accepted?" deterministically - computed in JS, not shipped as Dataview queries, so it works in a stock Obsidian with no plugins.--redactedis a second redaction on top of the<private>stripping every output already does: it dropsstakeholders.md,trust-profile.md, people pages, the trust column andsignal_*frontmatter, contact notes in the timeline, and[signal:x]/[@owner]tokens. The generated README states plainly what redaction does not do -decisions.md,risks.mdanddelivery.mdare shown as written, so judgement still applies before you screen-share.Because the command deletes its target before rebuilding,
resolveVaultOut()refuses anything it would be wrong to delete - the engagements root (either containment direction),$HOME, the filesystem root, any path with a.fdecomponent, a symlink, and any existing non-empty directory without the.fdeops-vaultstamp this command writes. One test drives all five refusals and then asserts the decoymy-notes/notes.mdstill exists.Files: new
bin/lib/vault.js(pure builders - data in,{rel, content}out, no fs/network),cmdVault+resolveVaultOut+valueLedgerRowsinbin/fde.js,vaultadded to the installer pass-through andcmdVaultto the CLI-wiring gate, one router row inSKILL.md, a USAGE section, 3.11.0 across the four manifests.npm run check: gates green, 116/116 (110 before, 6 new: portfolio/multi-engagement + private-leak sweep,--redacted, determinism + dropped engagement, the five refusals + a path with spaces, empty portfolio, unaccepted value inQuestions).Link to Devin session: https://app.devin.ai/sessions/f135381c4682413bae73dff38eb6d1a3
Open in Devin Desktop: https://app.devin.ai/desktop/session/f135381c4682413bae73dff38eb6d1a3?variant=devin
Requested by: @suboss87