@openparachute/vault 0.7.0-rc.9 → 0.7.2-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/src/aggregate.test.ts +260 -0
- package/core/src/contract-taxonomy.test.ts +26 -2
- package/core/src/contract-typed-index.test.ts +4 -2
- package/core/src/core.test.ts +737 -2
- package/core/src/cursor-keyset-ms.test.ts +537 -0
- package/core/src/cursor.ts +76 -6
- package/core/src/doctor.ts +23 -1
- package/core/src/hooks.ts +9 -0
- package/core/src/indexed-fields.test.ts +4 -1
- package/core/src/indexed-fields.ts +6 -1
- package/core/src/links.ts +22 -0
- package/core/src/mcp.ts +322 -53
- package/core/src/notes.ts +518 -67
- package/core/src/paths.ts +4 -0
- package/core/src/portable-md.test.ts +161 -0
- package/core/src/portable-md.ts +140 -9
- package/core/src/query-warnings.ts +60 -12
- package/core/src/schema-defaults.ts +7 -1
- package/core/src/schema.ts +128 -2
- package/core/src/seed-packs.ts +55 -15
- package/core/src/store.ts +141 -7
- package/core/src/tag-schemas.ts +20 -7
- package/core/src/txn.test.ts +100 -4
- package/core/src/txn.ts +119 -24
- package/core/src/types.ts +89 -0
- package/core/src/ulid.test.ts +56 -0
- package/core/src/ulid.ts +116 -0
- package/core/src/vault-projection.ts +19 -1
- package/core/src/wikilinks.test.ts +205 -1
- package/core/src/wikilinks.ts +200 -35
- package/package.json +1 -1
- package/src/aggregate-routes.test.ts +230 -0
- package/src/cli.ts +6 -0
- package/src/contract-errors.test.ts +2 -1
- package/src/contract-search.test.ts +17 -0
- package/src/mcp-link-warnings-scope.test.ts +144 -0
- package/src/mcp-query-notes-aggregate-scope.test.ts +183 -0
- package/src/mcp-tools.ts +76 -17
- package/src/mirror-import.ts +5 -0
- package/src/routes.ts +298 -24
- package/src/routing.test.ts +167 -0
- package/src/routing.ts +47 -11
- package/src/tag-integrity-mcp.test.ts +45 -6
- package/src/tag-scope.ts +10 -1
- package/src/vault.test.ts +557 -21
- package/web/ui/dist/assets/{index-B8pGeQam.js → index-CJ6NtIwh.js} +1 -1
- package/web/ui/dist/index.html +1 -1
package/src/mcp-tools.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { generateMcpTools } from "../core/src/mcp.ts";
|
|
9
9
|
import type { McpToolDef, GenerateMcpToolsOpts } from "../core/src/mcp.ts";
|
|
10
|
-
import { getNoteTags } from "../core/src/notes.ts";
|
|
10
|
+
import { getNoteTags, getVaultMap } from "../core/src/notes.ts";
|
|
11
11
|
import type { Note } from "../core/src/types.ts";
|
|
12
12
|
import {
|
|
13
13
|
buildVaultProjection,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
scrubTagFieldViolationsByScope,
|
|
29
29
|
scrubValidationStatusByScope,
|
|
30
30
|
tagsWithinScope,
|
|
31
|
+
tagVisibleInScope,
|
|
31
32
|
} from "./tag-scope.ts";
|
|
32
33
|
import { TagFieldConflictError, ParentCycleError } from "../core/src/tag-schemas.ts";
|
|
33
34
|
import { IndexedFieldError } from "../core/src/indexed-fields.ts";
|
|
@@ -131,7 +132,9 @@ function resolveVaultCoordinates(): { hubOrigin: string; hubOriginKnown: boolean
|
|
|
131
132
|
* `auth` is the resolved token for the caller and is captured by vault-info's
|
|
132
133
|
* execute closure so the description-update branch can perform a secondary
|
|
133
134
|
* scope check: the tool itself is gated at vault:read (so read-only callers
|
|
134
|
-
* can fetch stats), but writing a new description requires vault:
|
|
135
|
+
* can fetch stats), but writing a new description requires vault:admin
|
|
136
|
+
* (tightened from vault:write by the write/admin re-tier — description is
|
|
137
|
+
* vault curation, not content).
|
|
135
138
|
*
|
|
136
139
|
* When omitted (internal callers that only inspect the tool list — no execute
|
|
137
140
|
* path exercised), the description-update branch is disabled entirely.
|
|
@@ -158,6 +161,19 @@ export function generateScopedMcpTools(
|
|
|
158
161
|
? (note: Note) => noteWithinTagScope(note, allowedHolder.value, rawTags)
|
|
159
162
|
: undefined;
|
|
160
163
|
|
|
164
|
+
// Tag-scope for `query-notes`'s `aggregate` mode (top new-feature ask
|
|
165
|
+
// from a UX round): a rollup has no per-note shape to post-filter (the
|
|
166
|
+
// response is `[{group, value}]`, not notes), so scope has to be applied
|
|
167
|
+
// BEFORE core aggregates, not after. Same shared `allowedHolder` /
|
|
168
|
+
// `getAllowed()`-before-`orig()` ordering invariant as `expandVisibility`
|
|
169
|
+
// above — reads the resolved allowlist safely because the query-notes
|
|
170
|
+
// wrapper always awaits `getAllowed()` before invoking core's (synchronous
|
|
171
|
+
// use of this) execute. Unscoped sessions install no predicate → the
|
|
172
|
+
// aggregate runs core's fast direct-SQL path.
|
|
173
|
+
const aggregateVisibility = scoped
|
|
174
|
+
? (note: Note) => noteWithinTagScope(note, allowedHolder.value, rawTags)
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
161
177
|
// Tag-scope hop-guard for `near[]` (vault#439): a per-note predicate the
|
|
162
178
|
// core BFS consults so it refuses to traverse THROUGH out-of-scope notes —
|
|
163
179
|
// symmetric with find-path. Reads from the SAME shared `allowedHolder` the
|
|
@@ -211,11 +227,12 @@ export function generateScopedMcpTools(
|
|
|
211
227
|
|
|
212
228
|
const tools = generateMcpTools(
|
|
213
229
|
store,
|
|
214
|
-
expandVisibility || nearTraversable || ifExistsVisible || writeContext || strictBypass
|
|
230
|
+
expandVisibility || nearTraversable || ifExistsVisible || aggregateVisibility || writeContext || strictBypass
|
|
215
231
|
? {
|
|
216
232
|
...(expandVisibility ? { expandVisibility } : {}),
|
|
217
233
|
...(nearTraversable ? { nearTraversable } : {}),
|
|
218
234
|
...(ifExistsVisible ? { ifExistsVisible } : {}),
|
|
235
|
+
...(aggregateVisibility ? { aggregateVisibility } : {}),
|
|
219
236
|
...(writeContext ? { writeContext } : {}),
|
|
220
237
|
...(strictBypass ? { strictBypass } : {}),
|
|
221
238
|
...(onStrictBypass ? { onStrictBypass } : {}),
|
|
@@ -301,9 +318,11 @@ function applyTagDependencyGuards(tools: McpToolDef[], vaultName: string): void
|
|
|
301
318
|
* - vault-info: filter projection.tags + projection.indexed_fields
|
|
302
319
|
* to entries an in-scope tag contributes to
|
|
303
320
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
321
|
+
* Mutating-tool gating happens in handleScopedMcp at the verb-scope layer
|
|
322
|
+
* (`vault:write` for create/update/delete-note, `vault:admin` for the
|
|
323
|
+
* tag-schema/taxonomy tools since the write/admin re-tier) AND inside each
|
|
324
|
+
* tool's wrapper here, so a tag-scoped token — whatever verb it holds —
|
|
325
|
+
* can't mutate outside its allowlist. See applyTagScopeWriteGuards.
|
|
307
326
|
*/
|
|
308
327
|
function applyTagScopeWrappers(
|
|
309
328
|
tools: McpToolDef[],
|
|
@@ -365,6 +384,29 @@ function applyTagScopeWrappers(
|
|
|
365
384
|
const allowed = await getAllowed();
|
|
366
385
|
const result = await orig(params);
|
|
367
386
|
if (!allowed) return result;
|
|
387
|
+
// `aggregate` mode returns `[{group, value}]` rollup rows — no `.tags`
|
|
388
|
+
// to post-filter (and `noteWithinTagScope` would wrongly drop every
|
|
389
|
+
// row, since an absent `.tags` reads as "out of scope"). WHICH NOTES
|
|
390
|
+
// count was already narrowed INSIDE core via the `aggregateVisibility`
|
|
391
|
+
// predicate (see `generateScopedMcpTools`): fetch the full filtered
|
|
392
|
+
// set, keep only allowlisted notes, THEN aggregate.
|
|
393
|
+
//
|
|
394
|
+
// That note-level narrowing is NOT sufficient on its own under
|
|
395
|
+
// `group_by: "tag"`, though: a note can be in scope via one tag while
|
|
396
|
+
// ALSO carrying an out-of-scope co-tag (the same class of leak
|
|
397
|
+
// `scrubValidationStatusByScope` closes for `validation_status`), and a
|
|
398
|
+
// tag rollup's `group` values ARE tag names — the co-tag would surface
|
|
399
|
+
// directly as a group. Scrub group NAMES here, the same way every other
|
|
400
|
+
// tag-shaped output on this wrapper is scrubbed.
|
|
401
|
+
if ((params as any).aggregate) {
|
|
402
|
+
const groupBy = (params as any).aggregate?.group_by;
|
|
403
|
+
if (groupBy === "tag" && Array.isArray(result)) {
|
|
404
|
+
return result.filter(
|
|
405
|
+
(r: any) => typeof r?.group === "string" && tagVisibleInScope(r.group, allowed, rawTags),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
368
410
|
// Possible response shapes (vault#550 added the `warnings` variants):
|
|
369
411
|
// - Array (legacy list, no cursor, no warnings)
|
|
370
412
|
// - `{notes, next_cursor}` (cursor mode, vault#313)
|
|
@@ -478,20 +520,34 @@ function applyTagScopeWrappers(
|
|
|
478
520
|
tags: Array.isArray(r.tags) ? r.tags : [],
|
|
479
521
|
indexed_fields: Array.isArray(r.indexed_fields) ? r.indexed_fields : [],
|
|
480
522
|
query_hints: Array.isArray(r.query_hints) ? r.query_hints : [],
|
|
523
|
+
// Unused by filterProjectionByScope (map is handled separately below,
|
|
524
|
+
// via a live recompute) — a placeholder to satisfy VaultProjection's
|
|
525
|
+
// shape.
|
|
526
|
+
map: { total_notes: 0, tags: [], path_buckets: [], unfiled_notes: 0 },
|
|
481
527
|
};
|
|
482
528
|
const filtered = filterProjectionByScope(partial, allowed);
|
|
483
529
|
r.tags = filtered.tags;
|
|
484
530
|
r.indexed_fields = filtered.indexed_fields;
|
|
531
|
+
// `map` (front-door structural rollup): post-hoc filtering the unscoped
|
|
532
|
+
// aggregate isn't possible for path-bucket counts (they need per-note
|
|
533
|
+
// tag membership, not just a tag-name allowlist over a precomputed
|
|
534
|
+
// rollup) — so re-run the cheap grouped-count query restricted to the
|
|
535
|
+
// resolved allowlist instead of filtering `orig`'s unscoped result.
|
|
536
|
+
if (r.map) {
|
|
537
|
+
r.map = getVaultMap(store.db, { tagFilter: [...allowed] });
|
|
538
|
+
}
|
|
485
539
|
return r;
|
|
486
540
|
});
|
|
487
541
|
|
|
488
542
|
// ---- Write-side guards ----
|
|
489
543
|
//
|
|
490
|
-
// The verb-scope check (`vault:write`
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
// tag
|
|
544
|
+
// The verb-scope check (`vault:write` for note ops, `vault:admin` for the
|
|
545
|
+
// tag-schema/taxonomy ops post-re-tier) is enforced at the dispatch layer
|
|
546
|
+
// in handleScopedMcp. These wrappers add the second axis: a scoped token
|
|
547
|
+
// can only mutate within its tag-allowlist, never outside it. Tag
|
|
548
|
+
// operations (`update-tag`, `delete-tag`, `rename-tag`, `merge-tags`) gate
|
|
549
|
+
// on the tag name(s) themselves; note operations gate on the prospective
|
|
550
|
+
// tag set.
|
|
495
551
|
|
|
496
552
|
const forbidden = (msg: string): unknown => ({
|
|
497
553
|
error: "Forbidden",
|
|
@@ -709,13 +765,15 @@ function overrideVaultInfo(
|
|
|
709
765
|
|
|
710
766
|
if (params.description !== undefined) {
|
|
711
767
|
// Secondary scope check: vault-info is read-gated so read-only callers
|
|
712
|
-
// can fetch stats, but mutating the vault description requires
|
|
713
|
-
// for THIS vault
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
768
|
+
// can fetch stats, but mutating the vault description requires admin
|
|
769
|
+
// for THIS vault (was `write` — tightened by the write/admin re-tier:
|
|
770
|
+
// writing the vault's own description/config is curation, same class
|
|
771
|
+
// as update-tag et al, not content authorship). Without this, a
|
|
772
|
+
// vault:read OR vault:write token could bypass the outer gate by
|
|
773
|
+
// passing `description` to a tool the outer gate considers read-only.
|
|
774
|
+
if (!auth || !hasScopeForVault(auth.scopes, vaultName, "admin")) {
|
|
717
775
|
throw new Error(
|
|
718
|
-
`Forbidden: updating the vault description requires the 'vault:
|
|
776
|
+
`Forbidden: updating the vault description requires the 'vault:admin' scope (or 'vault:${vaultName}:admin'). Granted scopes: ${auth?.scopes.join(" ") || "(none)"}.`,
|
|
719
777
|
);
|
|
720
778
|
}
|
|
721
779
|
config.description = params.description as string;
|
|
@@ -747,6 +805,7 @@ function overrideVaultInfo(
|
|
|
747
805
|
tags: projection.tags,
|
|
748
806
|
indexed_fields: projection.indexed_fields,
|
|
749
807
|
query_hints: projection.query_hints,
|
|
808
|
+
map: projection.map,
|
|
750
809
|
};
|
|
751
810
|
|
|
752
811
|
// A2: surface a pointer (path, not body) to the seeded onboarding guide so
|
package/src/mirror-import.ts
CHANGED
|
@@ -523,6 +523,11 @@ function importResultFromStats(
|
|
|
523
523
|
`Dropped parent_names on tag "${sp.tag}": ${sp.reason}`,
|
|
524
524
|
);
|
|
525
525
|
}
|
|
526
|
+
for (const sd of stats.skipped_duplicate_ids) {
|
|
527
|
+
warnings.push(
|
|
528
|
+
`Skipped file (id "${sd.id}", path=${sd.path ?? "—"}): ${sd.reason}`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
526
531
|
const result: ImportResult = {
|
|
527
532
|
notes_imported: stats.notes_created + stats.notes_updated,
|
|
528
533
|
tags_imported: stats.schemas_restored,
|