@sparkelf/dsh-knowledge-base 0.1.21 → 0.1.23
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/README.md +32 -0
- package/lib/client.js +1228 -851
- package/lib/client.js.map +1 -1
- package/lib/index.js +497 -115
- package/lib/types/index.d.ts +24 -1
- package/lib/types/knowledgeTools.d.ts +12 -10
- package/lib/types/managementTools.d.ts +11 -7
- package/lib/types/toolDisclosure.d.ts +188 -0
- package/package.json +7 -6
package/lib/index.js
CHANGED
|
@@ -14,11 +14,17 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
14
14
|
* Model-facing management tools for the knowledge base and the workflow plaza.
|
|
15
15
|
*
|
|
16
16
|
* The UI already exposes these operations. This module gives the model the same
|
|
17
|
-
* operations without re-implementing authorization:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* operations without re-implementing authorization: which tools exist is decided
|
|
18
|
+
* once from the caller's review context by `toolDisclosure.ts`, so a tool body does
|
|
19
|
+
* not re-read the permission tree on every call. The context is still read where it
|
|
20
|
+
* decides WHICH rows a visible tool may return (department scope, own audit trail)
|
|
21
|
+
* and for the actor recorded in an audit line. Writes additionally require an
|
|
22
|
+
* explicit human authorization flag, because a model deciding on its own must never
|
|
23
|
+
* be indistinguishable from an operator approving a review.
|
|
24
|
+
*
|
|
25
|
+
* DataOps remains the final authority: every host call below reaches a backend
|
|
26
|
+
* route that enforces the JWT's current roles and grants, so a stale disclosure can
|
|
27
|
+
* cause a refusal but never an unauthorized operation.
|
|
22
28
|
*
|
|
23
29
|
* Secrets are write-only through the UI; these tools deliberately reject secret-shaped
|
|
24
30
|
* fields and redact them on reads.
|
|
@@ -99,10 +105,6 @@ function defaultPermissionsForRoles(roleKeys) {
|
|
|
99
105
|
function hasPermission(context, permission) {
|
|
100
106
|
return context.permissions.includes(permission);
|
|
101
107
|
}
|
|
102
|
-
/** Require one backend permission key; a denial is a structured 403. */
|
|
103
|
-
function requirePermission$1(context, permission, action) {
|
|
104
|
-
if (!hasPermission(context, permission)) throw new ManagementToolError("forbidden", `缺少权限 ${permission}${action === void 0 ? "" : "(" + action + ")"}`);
|
|
105
|
-
}
|
|
106
108
|
/** Global administrators: all resources and all review queues. */
|
|
107
109
|
function isGlobalAdmin(context) {
|
|
108
110
|
return context.review.canReviewGlobal === true || context.dataScope.all === true || context.roleCodes.some((code) => code === "SUPER_ADMIN" || code === "ADMIN");
|
|
@@ -217,7 +219,6 @@ const createManagementTools = (host) => [
|
|
|
217
219
|
parameters: {},
|
|
218
220
|
output: jsonOutput$1,
|
|
219
221
|
execute: async () => run$2(async () => {
|
|
220
|
-
requirePermission$1(await host.context(), "kb:config:manage");
|
|
221
222
|
return { config: redactSecrets(await host.kbConfigGet()) };
|
|
222
223
|
})
|
|
223
224
|
}),
|
|
@@ -235,7 +236,6 @@ const createManagementTools = (host) => [
|
|
|
235
236
|
},
|
|
236
237
|
output: jsonOutput$1,
|
|
237
238
|
execute: async (args) => run$2(async () => {
|
|
238
|
-
requirePermission$1(await host.context(), "kb:config:manage");
|
|
239
239
|
requireConfirmed(args.authorization);
|
|
240
240
|
assertNoSecretFields(args.patch);
|
|
241
241
|
return { config: redactSecrets(await host.kbConfigUpdate(args.patch)) };
|
|
@@ -248,7 +248,6 @@ const createManagementTools = (host) => [
|
|
|
248
248
|
output: jsonOutput$1,
|
|
249
249
|
execute: async () => run$2(async () => {
|
|
250
250
|
const context = await host.context();
|
|
251
|
-
requirePermission$1(context, "kb:grant:manage");
|
|
252
251
|
const grants = await host.kbGrantList();
|
|
253
252
|
return { grants: isGlobalAdmin(context) ? grants : grants.filter((grant) => isPathInDepartmentScope(context, grant.relPath)) };
|
|
254
253
|
})
|
|
@@ -281,9 +280,7 @@ const createManagementTools = (host) => [
|
|
|
281
280
|
},
|
|
282
281
|
output: jsonOutput$1,
|
|
283
282
|
execute: async (args) => run$2(async () => {
|
|
284
|
-
|
|
285
|
-
requirePermission$1(context, "kb:grant:manage");
|
|
286
|
-
requirePathScope(context, args.relPath);
|
|
283
|
+
requirePathScope(await host.context(), args.relPath);
|
|
287
284
|
requireConfirmed(args.authorization);
|
|
288
285
|
if (args.relPath.trim() === "" || args.principalId.trim() === "") throw new ManagementToolError("invalid", "relPath and principalId are required");
|
|
289
286
|
return { grant: await host.kbGrantSet(args) };
|
|
@@ -321,7 +318,6 @@ const createManagementTools = (host) => [
|
|
|
321
318
|
output: jsonOutput$1,
|
|
322
319
|
execute: async (args) => run$2(async () => {
|
|
323
320
|
const context = await host.context();
|
|
324
|
-
requirePermission$1(context, "kb:audit:read");
|
|
325
321
|
const entries = await host.kbAuditList({
|
|
326
322
|
...args,
|
|
327
323
|
limit: Math.max(1, Math.min(500, args.limit ?? 100))
|
|
@@ -336,7 +332,6 @@ const createManagementTools = (host) => [
|
|
|
336
332
|
output: jsonOutput$1,
|
|
337
333
|
execute: async () => run$2(async () => {
|
|
338
334
|
const context = await host.context();
|
|
339
|
-
requirePermission$1(context, "kb:review:decide");
|
|
340
335
|
const pending = await host.kbReviewList();
|
|
341
336
|
return { pending: isGlobalAdmin(context) ? pending : pending.filter((item) => isPathInDepartmentScope(context, item.relPath)) };
|
|
342
337
|
})
|
|
@@ -364,7 +359,6 @@ const createManagementTools = (host) => [
|
|
|
364
359
|
output: jsonOutput$1,
|
|
365
360
|
execute: async (args) => run$2(async () => {
|
|
366
361
|
const context = await host.context();
|
|
367
|
-
requirePermission$1(context, "kb:review:decide");
|
|
368
362
|
requireConfirmed(args.authorization);
|
|
369
363
|
if (args.decision !== "approve" && args.decision !== "reject") throw new ManagementToolError("invalid", "decision must be approve or reject");
|
|
370
364
|
const target = (await host.kbReviewList()).find((item) => item.name === args.name);
|
|
@@ -383,7 +377,6 @@ const createManagementTools = (host) => [
|
|
|
383
377
|
parameters: {},
|
|
384
378
|
output: jsonOutput$1,
|
|
385
379
|
execute: async () => run$2(async () => {
|
|
386
|
-
requirePermission$1(await host.context(), "kb:file:extract");
|
|
387
380
|
return { sync: await host.kbSync() };
|
|
388
381
|
})
|
|
389
382
|
}),
|
|
@@ -393,7 +386,6 @@ const createManagementTools = (host) => [
|
|
|
393
386
|
parameters: {},
|
|
394
387
|
output: jsonOutput$1,
|
|
395
388
|
execute: async () => run$2(async () => {
|
|
396
|
-
requirePermission$1(await host.context(), "kb:file:extract");
|
|
397
389
|
return { index: await host.kbIndexRebuild() };
|
|
398
390
|
})
|
|
399
391
|
}),
|
|
@@ -412,7 +404,6 @@ const createManagementTools = (host) => [
|
|
|
412
404
|
},
|
|
413
405
|
output: jsonOutput$1,
|
|
414
406
|
execute: async (args) => run$2(async () => {
|
|
415
|
-
requirePermission$1(await host.context(), "workflow:read");
|
|
416
407
|
return { workflows: await host.workflowList(args) };
|
|
417
408
|
})
|
|
418
409
|
}),
|
|
@@ -449,7 +440,6 @@ const createManagementTools = (host) => [
|
|
|
449
440
|
},
|
|
450
441
|
output: jsonOutput$1,
|
|
451
442
|
execute: async (args) => run$2(async () => {
|
|
452
|
-
requirePermission$1(await host.context(), "workflow:publish");
|
|
453
443
|
requireConfirmed(args.authorization);
|
|
454
444
|
return { workflow: await host.workflowUpdate(args) };
|
|
455
445
|
})
|
|
@@ -467,7 +457,6 @@ const createManagementTools = (host) => [
|
|
|
467
457
|
},
|
|
468
458
|
output: jsonOutput$1,
|
|
469
459
|
execute: async (args) => run$2(async () => {
|
|
470
|
-
requirePermission$1(await host.context(), "workflow:manage");
|
|
471
460
|
requireConfirmed(args.authorization);
|
|
472
461
|
return { result: await host.workflowUnpublish(args.id) };
|
|
473
462
|
})
|
|
@@ -491,7 +480,6 @@ const createManagementTools = (host) => [
|
|
|
491
480
|
},
|
|
492
481
|
output: jsonOutput$1,
|
|
493
482
|
execute: async (args) => run$2(async () => {
|
|
494
|
-
requirePermission$1(await host.context(), "workflow:review:decide");
|
|
495
483
|
return { workflows: await host.workflowReviewList(args) };
|
|
496
484
|
})
|
|
497
485
|
}),
|
|
@@ -505,7 +493,6 @@ const createManagementTools = (host) => [
|
|
|
505
493
|
} },
|
|
506
494
|
output: jsonOutput$1,
|
|
507
495
|
execute: async (args) => run$2(async () => {
|
|
508
|
-
requirePermission$1(await host.context(), "workflow:read");
|
|
509
496
|
if (!Array.isArray(args.ids) || args.ids.length === 0) throw new ManagementToolError("invalid", "ids is required");
|
|
510
497
|
return { scan: await host.workflowReviewScan(args.ids) };
|
|
511
498
|
})
|
|
@@ -532,7 +519,6 @@ const createManagementTools = (host) => [
|
|
|
532
519
|
},
|
|
533
520
|
output: jsonOutput$1,
|
|
534
521
|
execute: async (args) => run$2(async () => {
|
|
535
|
-
requirePermission$1(await host.context(), "workflow:review:decide");
|
|
536
522
|
requireConfirmed(args.authorization);
|
|
537
523
|
if (args.decision !== "approve" && args.decision !== "reject") throw new ManagementToolError("invalid", "decision must be approve or reject");
|
|
538
524
|
const ids = Array.isArray(args.ids) ? args.ids.filter((id) => typeof id === "string" && id !== "") : [];
|
|
@@ -546,6 +532,355 @@ const createManagementTools = (host) => [
|
|
|
546
532
|
})
|
|
547
533
|
];
|
|
548
534
|
//#endregion
|
|
535
|
+
//#region src/toolDisclosure.ts
|
|
536
|
+
/** The four disclosure groups a knowledge-base tool can belong to. */
|
|
537
|
+
const TOOL_GROUPS = [
|
|
538
|
+
"read",
|
|
539
|
+
"write",
|
|
540
|
+
"manage",
|
|
541
|
+
"publish"
|
|
542
|
+
];
|
|
543
|
+
/**
|
|
544
|
+
* Disclosure rules for every model-facing tool this package registers.
|
|
545
|
+
*
|
|
546
|
+
* The map is the single list of what exists; the coordinator rejects a tool
|
|
547
|
+
* definition without a rule and a rule without a definition at construction time,
|
|
548
|
+
* so a new tool cannot ship undisclosed or leave a stale policy behind.
|
|
549
|
+
*/
|
|
550
|
+
const KB_TOOL_ACCESS = Object.freeze({
|
|
551
|
+
kb_search: {
|
|
552
|
+
group: "read",
|
|
553
|
+
permissions: ["kb:file:read"]
|
|
554
|
+
},
|
|
555
|
+
kb_chunk_get: {
|
|
556
|
+
group: "read",
|
|
557
|
+
permissions: ["kb:file:read"]
|
|
558
|
+
},
|
|
559
|
+
kb_outline: {
|
|
560
|
+
group: "read",
|
|
561
|
+
permissions: ["kb:file:read"]
|
|
562
|
+
},
|
|
563
|
+
kb_outline_rewrite: {
|
|
564
|
+
group: "read",
|
|
565
|
+
permissions: ["kb:file:read"]
|
|
566
|
+
},
|
|
567
|
+
kb_vectorize: {
|
|
568
|
+
group: "read",
|
|
569
|
+
permissions: ["kb:file:read"]
|
|
570
|
+
},
|
|
571
|
+
workflow_list: {
|
|
572
|
+
group: "read",
|
|
573
|
+
permissions: ["workflow:read"]
|
|
574
|
+
},
|
|
575
|
+
kb_file_write: {
|
|
576
|
+
group: "write",
|
|
577
|
+
permissions: ["kb:file:write"]
|
|
578
|
+
},
|
|
579
|
+
kb_file_move: {
|
|
580
|
+
group: "write",
|
|
581
|
+
permissions: ["kb:file:write"]
|
|
582
|
+
},
|
|
583
|
+
kb_file_delete: {
|
|
584
|
+
group: "write",
|
|
585
|
+
permissions: ["kb:file:delete"]
|
|
586
|
+
},
|
|
587
|
+
kb_file_mkdir: {
|
|
588
|
+
group: "write",
|
|
589
|
+
permissions: ["kb:file:write"]
|
|
590
|
+
},
|
|
591
|
+
kb_file_extract: {
|
|
592
|
+
group: "write",
|
|
593
|
+
permissions: ["kb:file:extract"]
|
|
594
|
+
},
|
|
595
|
+
kb_outline_save: {
|
|
596
|
+
group: "write",
|
|
597
|
+
permissions: ["kb:file:read", "kb:file:write"]
|
|
598
|
+
},
|
|
599
|
+
kb_config_get: {
|
|
600
|
+
group: "manage",
|
|
601
|
+
permissions: ["kb:config:manage"]
|
|
602
|
+
},
|
|
603
|
+
kb_config_update: {
|
|
604
|
+
group: "manage",
|
|
605
|
+
permissions: ["kb:config:manage"]
|
|
606
|
+
},
|
|
607
|
+
kb_grant_list: {
|
|
608
|
+
group: "manage",
|
|
609
|
+
permissions: ["kb:grant:manage"]
|
|
610
|
+
},
|
|
611
|
+
kb_grant_set: {
|
|
612
|
+
group: "manage",
|
|
613
|
+
permissions: ["kb:grant:manage"]
|
|
614
|
+
},
|
|
615
|
+
kb_audit_list: {
|
|
616
|
+
group: "manage",
|
|
617
|
+
permissions: ["kb:audit:read"]
|
|
618
|
+
},
|
|
619
|
+
kb_review_list: {
|
|
620
|
+
group: "manage",
|
|
621
|
+
permissions: ["kb:review:decide"]
|
|
622
|
+
},
|
|
623
|
+
kb_review_decide: {
|
|
624
|
+
group: "manage",
|
|
625
|
+
permissions: ["kb:review:decide"]
|
|
626
|
+
},
|
|
627
|
+
kb_sync: {
|
|
628
|
+
group: "manage",
|
|
629
|
+
capability: "reviewer"
|
|
630
|
+
},
|
|
631
|
+
kb_index_rebuild: {
|
|
632
|
+
group: "manage",
|
|
633
|
+
permissions: ["kb:config:manage"]
|
|
634
|
+
},
|
|
635
|
+
workflow_unpublish: {
|
|
636
|
+
group: "manage",
|
|
637
|
+
permissions: ["workflow:manage"]
|
|
638
|
+
},
|
|
639
|
+
workflow_review_list: {
|
|
640
|
+
group: "manage",
|
|
641
|
+
permissions: ["workflow:review:decide"]
|
|
642
|
+
},
|
|
643
|
+
workflow_review_scan: {
|
|
644
|
+
group: "manage",
|
|
645
|
+
permissions: ["workflow:review:decide"]
|
|
646
|
+
},
|
|
647
|
+
workflow_review_decide: {
|
|
648
|
+
group: "manage",
|
|
649
|
+
permissions: ["workflow:review:decide"]
|
|
650
|
+
},
|
|
651
|
+
workflow_publish: {
|
|
652
|
+
group: "publish",
|
|
653
|
+
permissions: ["workflow:publish"]
|
|
654
|
+
},
|
|
655
|
+
workflow_update: {
|
|
656
|
+
group: "publish",
|
|
657
|
+
permissions: ["workflow:publish"]
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
Object.freeze({
|
|
661
|
+
read: namesIn("read"),
|
|
662
|
+
write: namesIn("write"),
|
|
663
|
+
manage: namesIn("manage"),
|
|
664
|
+
publish: namesIn("publish")
|
|
665
|
+
});
|
|
666
|
+
Object.freeze(Object.keys(KB_TOOL_ACCESS));
|
|
667
|
+
function namesIn(group) {
|
|
668
|
+
return Object.freeze(Object.entries(KB_TOOL_ACCESS).filter(([, rule]) => rule.group === group).map(([name]) => name));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Decide whether one tool is visible for a permission context.
|
|
672
|
+
*
|
|
673
|
+
* An absent context is a denial: disclosure happens before execution, so an
|
|
674
|
+
* unresolved permission tree must withhold the surface rather than expose it.
|
|
675
|
+
* Unknown tool names are denied too; the coordinator rejects them at construction,
|
|
676
|
+
* and this function keeps the same fail-closed answer if one reaches it anyway.
|
|
677
|
+
*
|
|
678
|
+
* @param name - model-facing tool name.
|
|
679
|
+
* @param context - the caller's review context, or undefined while it is unavailable.
|
|
680
|
+
* @returns the decision and the requirements the context did not satisfy.
|
|
681
|
+
*/
|
|
682
|
+
function decideToolAccess(name, context) {
|
|
683
|
+
const rule = KB_TOOL_ACCESS[name];
|
|
684
|
+
if (rule === void 0) return {
|
|
685
|
+
allowed: false,
|
|
686
|
+
missing: ["disclosure-policy"]
|
|
687
|
+
};
|
|
688
|
+
if (context === void 0) return {
|
|
689
|
+
allowed: false,
|
|
690
|
+
group: rule.group,
|
|
691
|
+
missing: ["permission-context"]
|
|
692
|
+
};
|
|
693
|
+
const missing = [];
|
|
694
|
+
for (const permission of rule.permissions ?? []) if (!hasPermission(context, permission)) missing.push(permission);
|
|
695
|
+
if (rule.capability === "reviewer" && !(isGlobalAdmin(context) || context.review.canReview === true)) missing.push("reviewer");
|
|
696
|
+
if (rule.capability === "admin" && !isGlobalAdmin(context)) missing.push("admin");
|
|
697
|
+
return {
|
|
698
|
+
allowed: missing.length === 0,
|
|
699
|
+
group: rule.group,
|
|
700
|
+
missing
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Register and dispose knowledge-base tools as the caller's permission context changes.
|
|
705
|
+
*
|
|
706
|
+
* The coordinator owns every tool registration. A pass computes one decision per
|
|
707
|
+
* tool, registers the newly allowed ones, disposes the newly denied ones and
|
|
708
|
+
* leaves unchanged tools untouched, so an unchanged context causes no registry
|
|
709
|
+
* churn. Concurrent refreshes share one in-flight context read; a pass that is
|
|
710
|
+
* superseded by a newer context never applies its stale answer.
|
|
711
|
+
*
|
|
712
|
+
* On a context-resolution failure every gated tool is disposed. That is the
|
|
713
|
+
* fail-closed direction: an unavailable permission tree cannot prove a capability,
|
|
714
|
+
* and a tool surface that outlives its proof is exactly the leak disclosure exists
|
|
715
|
+
* to prevent.
|
|
716
|
+
*/
|
|
717
|
+
var ToolDisclosureCoordinator = class {
|
|
718
|
+
definitions;
|
|
719
|
+
/** Registered tools and their exact registry disposers. */
|
|
720
|
+
disposers = /* @__PURE__ */ new Map();
|
|
721
|
+
options;
|
|
722
|
+
/** Monotonic pass counter; a slower older read must not overwrite a newer one. */
|
|
723
|
+
generation = 0;
|
|
724
|
+
inFlight;
|
|
725
|
+
disposed = false;
|
|
726
|
+
snapshot;
|
|
727
|
+
constructor(options) {
|
|
728
|
+
this.options = options;
|
|
729
|
+
this.definitions = new Map(options.tools.map((definition) => [definition.name, definition]));
|
|
730
|
+
assertCompleteDisclosure(this.definitions.keys());
|
|
731
|
+
}
|
|
732
|
+
/** Tool names currently registered, in definition order. */
|
|
733
|
+
get registeredTools() {
|
|
734
|
+
return [...this.definitions.keys()].filter((name) => this.disposers.has(name));
|
|
735
|
+
}
|
|
736
|
+
/** The most recent disclosure pass, if one has completed. */
|
|
737
|
+
get lastSnapshot() {
|
|
738
|
+
return this.snapshot;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Recompute the tool surface.
|
|
742
|
+
*
|
|
743
|
+
* @param context - a context already read by the caller (a route gate or status
|
|
744
|
+
* request). Passing it avoids a second backend round trip. Omit it to resolve
|
|
745
|
+
* one through {@link ToolDisclosureOptions.resolveContext}.
|
|
746
|
+
* @returns the resulting snapshot; a context failure is reported in `error`
|
|
747
|
+
* rather than thrown, because a refresh must never fail the request that
|
|
748
|
+
* triggered it.
|
|
749
|
+
*/
|
|
750
|
+
syncToolDisclosure(context) {
|
|
751
|
+
if (this.disposed) return Promise.resolve(this.emptySnapshot("tool disclosure is disposed"));
|
|
752
|
+
if (context !== void 0) {
|
|
753
|
+
this.generation += 1;
|
|
754
|
+
try {
|
|
755
|
+
return Promise.resolve(this.apply(context));
|
|
756
|
+
} catch (error) {
|
|
757
|
+
return Promise.resolve(this.fail(error));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (this.inFlight !== void 0) return this.inFlight;
|
|
761
|
+
const generation = ++this.generation;
|
|
762
|
+
const run = Promise.resolve().then(() => this.options.resolveContext()).then((resolved) => generation === this.generation ? this.apply(resolved) : this.snapshot ?? this.emptySnapshot("a newer disclosure pass superseded this one")).catch((error) => generation === this.generation ? this.fail(error) : this.snapshot ?? this.emptySnapshot("a newer disclosure pass superseded this one")).finally(() => {
|
|
763
|
+
if (this.inFlight === run) this.inFlight = void 0;
|
|
764
|
+
});
|
|
765
|
+
this.inFlight = run;
|
|
766
|
+
return run;
|
|
767
|
+
}
|
|
768
|
+
/** Dispose every tool this coordinator registered; later refreshes are no-ops. */
|
|
769
|
+
dispose() {
|
|
770
|
+
this.disposed = true;
|
|
771
|
+
this.disposeRegistered();
|
|
772
|
+
}
|
|
773
|
+
/** Register allowed tools and dispose denied ones for one resolved context. */
|
|
774
|
+
apply(context) {
|
|
775
|
+
const denied = [];
|
|
776
|
+
for (const [name, definition] of this.definitions) {
|
|
777
|
+
const decision = decideToolAccess(name, context);
|
|
778
|
+
if (decision.allowed) {
|
|
779
|
+
if (!this.disposers.has(name)) try {
|
|
780
|
+
this.disposers.set(name, this.options.registry.register(definition));
|
|
781
|
+
} catch (error) {
|
|
782
|
+
this.options.log?.(`tool disclosure: could not register ${name}: ${messageOf$1(error)}`);
|
|
783
|
+
denied.push({
|
|
784
|
+
name,
|
|
785
|
+
...decision.group === void 0 ? {} : { group: decision.group },
|
|
786
|
+
missing: ["register-failed"]
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
denied.push({
|
|
792
|
+
name,
|
|
793
|
+
...decision.group === void 0 ? {} : { group: decision.group },
|
|
794
|
+
missing: decision.missing
|
|
795
|
+
});
|
|
796
|
+
const dispose = this.disposers.get(name);
|
|
797
|
+
if (dispose !== void 0) {
|
|
798
|
+
try {
|
|
799
|
+
dispose();
|
|
800
|
+
} catch (error) {
|
|
801
|
+
this.options.log?.(`tool disclosure: could not dispose ${name}: ${messageOf$1(error)}`);
|
|
802
|
+
}
|
|
803
|
+
this.disposers.delete(name);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const registered = this.registeredTools;
|
|
807
|
+
const snapshot = {
|
|
808
|
+
registered,
|
|
809
|
+
denied,
|
|
810
|
+
activeGroups: TOOL_GROUPS.filter((group) => registered.some((name) => KB_TOOL_ACCESS[name]?.group === group))
|
|
811
|
+
};
|
|
812
|
+
const changed = this.snapshot === void 0 || this.snapshot.registered.join("\0") !== registered.join("\0") || this.snapshot.error === void 0 !== (snapshot.error === void 0);
|
|
813
|
+
this.snapshot = snapshot;
|
|
814
|
+
if (changed) this.options.log?.(`tool disclosure: registered ${String(registered.length)} tool(s) [${registered.join(", ")}]; withheld ${String(denied.length)}`);
|
|
815
|
+
return snapshot;
|
|
816
|
+
}
|
|
817
|
+
/** Dispose everything and report why the surface is empty. */
|
|
818
|
+
fail(error) {
|
|
819
|
+
this.disposeRegistered();
|
|
820
|
+
const message = messageOf$1(error);
|
|
821
|
+
const snapshot = {
|
|
822
|
+
registered: [],
|
|
823
|
+
denied: [...this.definitions.keys()].map((name) => ({
|
|
824
|
+
name,
|
|
825
|
+
...KB_TOOL_ACCESS[name] === void 0 ? {} : { group: KB_TOOL_ACCESS[name].group },
|
|
826
|
+
missing: ["permission-context"]
|
|
827
|
+
})),
|
|
828
|
+
activeGroups: [],
|
|
829
|
+
error: message
|
|
830
|
+
};
|
|
831
|
+
this.snapshot = snapshot;
|
|
832
|
+
this.options.log?.(`tool disclosure: permission context unavailable; withheld every gated tool: ${message}`);
|
|
833
|
+
return snapshot;
|
|
834
|
+
}
|
|
835
|
+
/** Dispose every registered tool; safe to call more than once. */
|
|
836
|
+
disposeRegistered() {
|
|
837
|
+
for (const dispose of this.disposers.values()) try {
|
|
838
|
+
dispose();
|
|
839
|
+
} catch (error) {
|
|
840
|
+
this.options.log?.(`tool disclosure: could not dispose a tool: ${messageOf$1(error)}`);
|
|
841
|
+
}
|
|
842
|
+
this.disposers.clear();
|
|
843
|
+
}
|
|
844
|
+
/** Snapshot for a pass that cannot run at all. */
|
|
845
|
+
emptySnapshot(reason) {
|
|
846
|
+
return {
|
|
847
|
+
registered: [],
|
|
848
|
+
denied: [...this.definitions.keys()].map((name) => ({
|
|
849
|
+
name,
|
|
850
|
+
...KB_TOOL_ACCESS[name] === void 0 ? {} : { group: KB_TOOL_ACCESS[name].group },
|
|
851
|
+
missing: ["permission-context"]
|
|
852
|
+
})),
|
|
853
|
+
activeGroups: [],
|
|
854
|
+
error: reason
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* Build a coordinator.
|
|
860
|
+
*
|
|
861
|
+
* @param options - the definitions, registry, context resolver and log sink.
|
|
862
|
+
* @returns the coordinator the plugin stores and refreshes.
|
|
863
|
+
*/
|
|
864
|
+
function createToolDisclosure(options) {
|
|
865
|
+
return new ToolDisclosureCoordinator(options);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Reject a policy map that drifted from the tool set.
|
|
869
|
+
*
|
|
870
|
+
* Called by the constructor, so a missing rule or a stale rule fails at plugin
|
|
871
|
+
* start rather than silently exposing or hiding a tool later.
|
|
872
|
+
*/
|
|
873
|
+
function assertCompleteDisclosure(names) {
|
|
874
|
+
const present = new Set(names);
|
|
875
|
+
const unknown = [...present].filter((name) => KB_TOOL_ACCESS[name] === void 0);
|
|
876
|
+
const stale = Object.keys(KB_TOOL_ACCESS).filter((name) => !present.has(name));
|
|
877
|
+
if (unknown.length > 0 || stale.length > 0) throw new Error("knowledge-base tool disclosure is incomplete:" + (unknown.length === 0 ? "" : ` no policy for ${unknown.join(", ")};`) + (stale.length === 0 ? "" : ` policy without a tool for ${stale.join(", ")};`));
|
|
878
|
+
}
|
|
879
|
+
/** Error text safe to log from a contained refresh. */
|
|
880
|
+
function messageOf$1(error) {
|
|
881
|
+
return error instanceof Error ? error.message : String(error);
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
549
884
|
//#region src/workflowPublish.ts
|
|
550
885
|
/**
|
|
551
886
|
* The workflow-plaza publish store.
|
|
@@ -8244,16 +8579,12 @@ const run = async (path, operation) => {
|
|
|
8244
8579
|
throw asKnowledgeToolError(error, path);
|
|
8245
8580
|
}
|
|
8246
8581
|
};
|
|
8247
|
-
/** Require one backend DSH permission before the tool touches the tree. */
|
|
8248
|
-
async function requirePermission(host, permission) {
|
|
8249
|
-
if (host.hasPermission === void 0) return;
|
|
8250
|
-
if (!await host.hasPermission(permission)) throw new KnowledgeToolError("forbidden", `缺少权限 ${permission}`);
|
|
8251
|
-
}
|
|
8252
8582
|
/**
|
|
8253
|
-
* Build the
|
|
8583
|
+
* Build the knowledge-base model-facing tools.
|
|
8254
8584
|
*
|
|
8255
|
-
* Returned definitions are registered by `apply
|
|
8256
|
-
*
|
|
8585
|
+
* Returned definitions are registered by `apply` through the permission-based
|
|
8586
|
+
* disclosure coordinator; tests call the same definitions with a fake host to assert
|
|
8587
|
+
* path, audit and result behaviour without a live backend or a permission backend.
|
|
8257
8588
|
*/
|
|
8258
8589
|
const createKnowledgeFileTools = (host) => [
|
|
8259
8590
|
defineTool({
|
|
@@ -8279,7 +8610,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8279
8610
|
execute: async (args) => run(args.path, async () => {
|
|
8280
8611
|
const relPath = assertToolPath(args.path);
|
|
8281
8612
|
const content = typeof args.content === "string" ? args.content : "";
|
|
8282
|
-
await requirePermission(host, "kb:file:write");
|
|
8283
8613
|
await requireWrite(host, relPath);
|
|
8284
8614
|
const caller = await host.resolveCaller();
|
|
8285
8615
|
const existing = await host.statEntry(relPath);
|
|
@@ -8344,7 +8674,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8344
8674
|
execute: async (args) => run(args.from, async () => {
|
|
8345
8675
|
const from = assertToolPath(args.from);
|
|
8346
8676
|
const to = assertToolPath(args.to, true);
|
|
8347
|
-
await requirePermission(host, "kb:file:write");
|
|
8348
8677
|
await requireWrite(host, from);
|
|
8349
8678
|
const caller = await host.resolveCaller();
|
|
8350
8679
|
const targetEntry = to === "" ? void 0 : await host.statEntry(to);
|
|
@@ -8391,7 +8720,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8391
8720
|
output: jsonOutput,
|
|
8392
8721
|
isConcurrencySafe: () => true,
|
|
8393
8722
|
execute: async (args) => run(void 0, async () => {
|
|
8394
|
-
await requirePermission(host, "kb:file:read");
|
|
8395
8723
|
if (host.searchIndex === void 0) throw new KnowledgeToolError("not-configured", "knowledge index is unavailable");
|
|
8396
8724
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
8397
8725
|
if (query === "") throw new KnowledgeToolError("empty-path", "query must be non-empty");
|
|
@@ -8482,7 +8810,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8482
8810
|
},
|
|
8483
8811
|
output: jsonOutput,
|
|
8484
8812
|
execute: async (args) => run(args.path, async () => {
|
|
8485
|
-
await requirePermission(host, "kb:file:read");
|
|
8486
8813
|
if (host.vectorize === void 0) throw new KnowledgeToolError("not-configured", "vector indexing is unavailable");
|
|
8487
8814
|
const hasDocId = args.docId !== void 0;
|
|
8488
8815
|
const hasPath = typeof args.path === "string" && args.path.trim() !== "";
|
|
@@ -8529,7 +8856,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8529
8856
|
output: jsonOutput,
|
|
8530
8857
|
isConcurrencySafe: () => true,
|
|
8531
8858
|
execute: async (args) => run(void 0, async () => {
|
|
8532
|
-
await requirePermission(host, "kb:file:read");
|
|
8533
8859
|
if (host.chunkGet === void 0) throw new KnowledgeToolError("not-configured", "knowledge chunk resolver is unavailable");
|
|
8534
8860
|
const fromToken = typeof args.citation === "string" ? /^kb:\/\/doc\/([^/\s?#]+)\/chunk\/([^/\s?#]+)/i.exec(args.citation.trim()) : null;
|
|
8535
8861
|
const docId = Number(fromToken?.[1] ?? args.docId);
|
|
@@ -8570,7 +8896,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8570
8896
|
output: jsonOutput,
|
|
8571
8897
|
isConcurrencySafe: () => true,
|
|
8572
8898
|
execute: async (args) => run(void 0, async () => {
|
|
8573
|
-
await requirePermission(host, "kb:file:read");
|
|
8574
8899
|
if (host.outline === void 0) throw new KnowledgeToolError("not-configured", "knowledge outline resolver is unavailable");
|
|
8575
8900
|
const rawToken = args.citation ?? args.citationToken ?? "";
|
|
8576
8901
|
const fromToken = rawToken.trim() !== "" ? /^kb:\/\/doc\/([^/\s?#]+)(?:\/chunk\/[^/\s?#]+)?/iu.exec(rawToken.trim()) : null;
|
|
@@ -8594,7 +8919,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8594
8919
|
output: jsonOutput,
|
|
8595
8920
|
isConcurrencySafe: () => false,
|
|
8596
8921
|
execute: async (args) => run(void 0, async () => {
|
|
8597
|
-
await requirePermission(host, "kb:file:read");
|
|
8598
8922
|
if (host.outlineRewrite === void 0) throw new KnowledgeToolError("not-configured", "knowledge outline rewrite is unavailable");
|
|
8599
8923
|
const docId = Number(args.docId);
|
|
8600
8924
|
if (!Number.isInteger(docId) || docId <= 0) throw new KnowledgeToolError("invalid-citation", "docId must be a positive integer");
|
|
@@ -8677,8 +9001,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8677
9001
|
},
|
|
8678
9002
|
output: jsonOutput,
|
|
8679
9003
|
execute: async (args) => run(void 0, async () => {
|
|
8680
|
-
await requirePermission(host, "kb:file:read");
|
|
8681
|
-
await requirePermission(host, "kb:file:write");
|
|
8682
9004
|
if (host.outlineSave === void 0) throw new KnowledgeToolError("not-configured", "knowledge outline saver is unavailable");
|
|
8683
9005
|
const docId = Number(args.docId);
|
|
8684
9006
|
if (!Number.isInteger(docId) || docId <= 0) throw new KnowledgeToolError("invalid-summary", "docId must be a positive integer");
|
|
@@ -8708,7 +9030,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8708
9030
|
output: jsonOutput,
|
|
8709
9031
|
execute: async (args) => run(args.path, async () => {
|
|
8710
9032
|
const relPath = assertToolPath(args.path);
|
|
8711
|
-
await requirePermission(host, "kb:file:delete");
|
|
8712
9033
|
await requireWrite(host, relPath);
|
|
8713
9034
|
const caller = await host.resolveCaller();
|
|
8714
9035
|
const entry = await host.statEntry(relPath);
|
|
@@ -8737,7 +9058,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8737
9058
|
output: jsonOutput,
|
|
8738
9059
|
execute: async (args) => run(args.path, async () => {
|
|
8739
9060
|
const relPath = assertToolPath(args.path);
|
|
8740
|
-
await requirePermission(host, "kb:file:write");
|
|
8741
9061
|
await requireWrite(host, relPath);
|
|
8742
9062
|
const caller = await host.resolveCaller();
|
|
8743
9063
|
const created = await host.createEntry(relPath, { directory: true });
|
|
@@ -8762,7 +9082,6 @@ const createKnowledgeFileTools = (host) => [
|
|
|
8762
9082
|
output: jsonOutput,
|
|
8763
9083
|
execute: async (args) => run(args.path, async () => {
|
|
8764
9084
|
const relPath = assertToolPath(args.path ?? "", true);
|
|
8765
|
-
await requirePermission(host, "kb:file:extract");
|
|
8766
9085
|
const caller = await host.resolveCaller();
|
|
8767
9086
|
if (relPath === "") {
|
|
8768
9087
|
const candidates = await host.listExtractCandidates(20);
|
|
@@ -8989,6 +9308,54 @@ function isTeamDeployment(config) {
|
|
|
8989
9308
|
*/
|
|
8990
9309
|
const LOCAL_USER_ID = "local";
|
|
8991
9310
|
/**
|
|
9311
|
+
* The permission context a personal deployment discloses tools from.
|
|
9312
|
+
*
|
|
9313
|
+
* A personal deployment has no accounts, so there is nobody to ask: the local
|
|
9314
|
+
* operator owns every capability the product knows about. Kept as a function rather
|
|
9315
|
+
* than the inline object inside the management host so the disclosure coordinator
|
|
9316
|
+
* can register the personal tool surface synchronously at startup, instead of
|
|
9317
|
+
* waiting for a backend round trip that does not exist.
|
|
9318
|
+
*
|
|
9319
|
+
* @returns the full-capability context a local owner holds.
|
|
9320
|
+
*/
|
|
9321
|
+
function localManagementContext() {
|
|
9322
|
+
return {
|
|
9323
|
+
userId: "local",
|
|
9324
|
+
username: "local",
|
|
9325
|
+
displayName: "个人用户",
|
|
9326
|
+
primaryRole: "LOCAL_OWNER",
|
|
9327
|
+
roleCodes: ["LOCAL_OWNER"],
|
|
9328
|
+
permissions: [
|
|
9329
|
+
"kb:file:read",
|
|
9330
|
+
"kb:file:write",
|
|
9331
|
+
"kb:file:delete",
|
|
9332
|
+
"kb:file:extract",
|
|
9333
|
+
"kb:audit:read",
|
|
9334
|
+
"kb:grant:manage",
|
|
9335
|
+
"kb:review:decide",
|
|
9336
|
+
"kb:config:manage",
|
|
9337
|
+
"workflow:read",
|
|
9338
|
+
"workflow:publish",
|
|
9339
|
+
"workflow:manage",
|
|
9340
|
+
"workflow:review:decide",
|
|
9341
|
+
"workflow:review:global"
|
|
9342
|
+
],
|
|
9343
|
+
departmentCode: null,
|
|
9344
|
+
departmentPathCodes: [],
|
|
9345
|
+
dataScope: {
|
|
9346
|
+
all: true,
|
|
9347
|
+
self: true,
|
|
9348
|
+
departmentCodes: []
|
|
9349
|
+
},
|
|
9350
|
+
review: {
|
|
9351
|
+
canReview: true,
|
|
9352
|
+
canReviewGlobal: true,
|
|
9353
|
+
canManagePermissions: true,
|
|
9354
|
+
departments: []
|
|
9355
|
+
}
|
|
9356
|
+
};
|
|
9357
|
+
}
|
|
9358
|
+
/**
|
|
8992
9359
|
* Whether a personal deployment may read or write one path.
|
|
8993
9360
|
*
|
|
8994
9361
|
* A personal deployment has no OTHER users to protect a path from, so there is no grant to
|
|
@@ -10837,6 +11204,24 @@ function knowledgeBaseSkillProvider() {
|
|
|
10837
11204
|
}
|
|
10838
11205
|
};
|
|
10839
11206
|
}
|
|
11207
|
+
/**
|
|
11208
|
+
* Per-plugin-instance disclosure coordinators, keyed by the Host context.
|
|
11209
|
+
*
|
|
11210
|
+
* `apply` is synchronous and the Host does not hand a plugin a return value, but
|
|
11211
|
+
* the coordinator must be reachable for two legitimate callers: the host integration
|
|
11212
|
+
* that wants to force a refresh, and the tests that drive `apply` directly. A
|
|
11213
|
+
* WeakMap keeps this a discovery seam rather than a second lifecycle owner.
|
|
11214
|
+
*/
|
|
11215
|
+
const toolDisclosures = /* @__PURE__ */ new WeakMap();
|
|
11216
|
+
/**
|
|
11217
|
+
* The permission-based tool disclosure coordinator this plugin instance registered.
|
|
11218
|
+
*
|
|
11219
|
+
* @param host - the Host context the plugin was applied to.
|
|
11220
|
+
* @returns the coordinator, or undefined when this context has no knowledge-base plugin.
|
|
11221
|
+
*/
|
|
11222
|
+
function knowledgeBaseToolDisclosure(host) {
|
|
11223
|
+
return toolDisclosures.get(host);
|
|
11224
|
+
}
|
|
10840
11225
|
function apply(ctx, deploymentConfig) {
|
|
10841
11226
|
const config = deploymentConfig;
|
|
10842
11227
|
const host = ctx;
|
|
@@ -11037,6 +11422,15 @@ function apply(ctx, deploymentConfig) {
|
|
|
11037
11422
|
...lastFailure === void 0 ? {} : { failure: lastFailure }
|
|
11038
11423
|
};
|
|
11039
11424
|
};
|
|
11425
|
+
/**
|
|
11426
|
+
* Permission-based tool disclosure, assigned once the host seams exist below.
|
|
11427
|
+
*
|
|
11428
|
+
* Declared before the HTTP surface because the route handler closes over it: the
|
|
11429
|
+
* route gate is one of the two existing places this plugin already reads the
|
|
11430
|
+
* caller's permission context, so it is also where a changed context refreshes the
|
|
11431
|
+
* model-facing tool surface.
|
|
11432
|
+
*/
|
|
11433
|
+
let toolDisclosure;
|
|
11040
11434
|
ctx.effect(() => host.webServer.register({
|
|
11041
11435
|
kind: "prefix",
|
|
11042
11436
|
path: "/knowledge-base/api",
|
|
@@ -11049,6 +11443,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
11049
11443
|
const permissionContext = await Promise.race([managementHost.context().catch(() => void 0), new Promise((resolve) => setTimeout(() => {
|
|
11050
11444
|
resolve(void 0);
|
|
11051
11445
|
}, 750))]);
|
|
11446
|
+
if (permissionContext !== void 0) await toolDisclosure?.syncToolDisclosure(permissionContext);
|
|
11052
11447
|
if (permissionContext !== void 0 && permissionContext.permissions.length > 0 && !hasPermission(permissionContext, requiredPermission)) return writeJson(res, 403, {
|
|
11053
11448
|
error: `缺少权限 ${requiredPermission}`,
|
|
11054
11449
|
code: "forbidden",
|
|
@@ -11257,21 +11652,18 @@ function apply(ctx, deploymentConfig) {
|
|
|
11257
11652
|
if (!initializer.hasSucceeded()) await initializer.ensure();
|
|
11258
11653
|
const canManage = isTeamDeployment(config) && await canManageGrants(host, config);
|
|
11259
11654
|
let permissionKeys = [];
|
|
11655
|
+
let permissionContext;
|
|
11260
11656
|
if (isTeamDeployment(config)) try {
|
|
11261
|
-
|
|
11657
|
+
permissionContext = await managementHost.context();
|
|
11658
|
+
permissionKeys = [...permissionContext.permissions];
|
|
11262
11659
|
} catch {
|
|
11263
11660
|
permissionKeys = [];
|
|
11264
11661
|
}
|
|
11265
|
-
else
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
"kb:audit:read",
|
|
11271
|
-
"kb:grant:manage",
|
|
11272
|
-
"kb:review:decide",
|
|
11273
|
-
"kb:config:manage"
|
|
11274
|
-
];
|
|
11662
|
+
else {
|
|
11663
|
+
permissionContext = localManagementContext();
|
|
11664
|
+
permissionKeys = [...permissionContext.permissions];
|
|
11665
|
+
}
|
|
11666
|
+
if (permissionContext !== void 0) await toolDisclosure?.syncToolDisclosure(permissionContext);
|
|
11275
11667
|
if (config.storageMode === "local") {
|
|
11276
11668
|
const localOnly = await localStatus(config);
|
|
11277
11669
|
let localCaller;
|
|
@@ -12679,7 +13071,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
12679
13071
|
if (typeof hostWithInject.inject === "function") hostWithInject.inject(["skills"], (scoped) => {
|
|
12680
13072
|
scoped.skills.registerProvider(() => knowledgeBaseSkillProvider());
|
|
12681
13073
|
});
|
|
12682
|
-
|
|
13074
|
+
const workflowPublishTool = defineTool({
|
|
12683
13075
|
name: "workflow_publish",
|
|
12684
13076
|
description: "Publish one workflow from this conversation to the Workflow Plaza. The record is created as PENDING; it does NOT appear in the public plaza until an administrator approves it. 发布后需管理员审核,通过后才出现在广场。Use this only when the user explicitly asks to publish/share the workflow. Do not use it to edit an existing workflow or to run a task now. Provide the standing instruction that will run on the chosen schedule, in the user's language. cronHint should be a 5-field cron expression when the user gave one; category is a short group label.",
|
|
12685
13077
|
parameters: {
|
|
@@ -12746,8 +13138,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
12746
13138
|
execute: async (args, exec) => {
|
|
12747
13139
|
const actor = await resolveWorkflowActor(host, config);
|
|
12748
13140
|
const context = await managementHost.context();
|
|
12749
|
-
|
|
12750
|
-
if (args.visibility === "global") requirePermission$1(context, "workflow:review:global", "global workflow publish");
|
|
13141
|
+
if (args.visibility === "global" && !hasPermission(context, "workflow:review:global")) throw new ManagementToolError("forbidden", "缺少权限 workflow:review:global(global workflow publish)");
|
|
12751
13142
|
const scope = normalizePublishScope(context, args.visibility, args.departmentCode);
|
|
12752
13143
|
const sessionId = exec.agent?.session.id;
|
|
12753
13144
|
const result = await publishWorkflow(publishStorePath(publishDirOf(config.publishDir)), {
|
|
@@ -12763,7 +13154,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
12763
13154
|
report: `${prefix} 管理员审核通过后才会出现在工作流广场;当前状态:待审核。`
|
|
12764
13155
|
};
|
|
12765
13156
|
}
|
|
12766
|
-
})
|
|
13157
|
+
});
|
|
12767
13158
|
/** Sidecar path, guarded against living inside the agent-visible tree. */
|
|
12768
13159
|
const knowledgeIndexPath = () => {
|
|
12769
13160
|
const plan = localConfig(config);
|
|
@@ -13111,14 +13502,6 @@ function apply(ctx, deploymentConfig) {
|
|
|
13111
13502
|
}
|
|
13112
13503
|
};
|
|
13113
13504
|
const knowledgeToolHost = {
|
|
13114
|
-
hasPermission: async (permission) => {
|
|
13115
|
-
if (!isTeamDeployment(config)) return true;
|
|
13116
|
-
try {
|
|
13117
|
-
return (await managementHost.context()).permissions.includes(permission);
|
|
13118
|
-
} catch {
|
|
13119
|
-
return true;
|
|
13120
|
-
}
|
|
13121
|
-
},
|
|
13122
13505
|
vectorize: async (input) => {
|
|
13123
13506
|
try {
|
|
13124
13507
|
await syncKnowledgeIndex(false, false);
|
|
@@ -13459,8 +13842,9 @@ function apply(ctx, deploymentConfig) {
|
|
|
13459
13842
|
* Management-tool host.
|
|
13460
13843
|
*
|
|
13461
13844
|
* Every method reads/writes through the same backend or host primitive the matching
|
|
13462
|
-
* UI surface uses. Role checks happen
|
|
13463
|
-
*
|
|
13845
|
+
* UI surface uses. Role checks happen once at tool disclosure; the context reads
|
|
13846
|
+
* that remain here feed data scoping and audit identity, and the backend still
|
|
13847
|
+
* refuses an operation the caller may not perform.
|
|
13464
13848
|
*/
|
|
13465
13849
|
const builtInWorkflowRows = [
|
|
13466
13850
|
{
|
|
@@ -13476,42 +13860,18 @@ function apply(ctx, deploymentConfig) {
|
|
|
13476
13860
|
title: "Archive stale personal documents"
|
|
13477
13861
|
}
|
|
13478
13862
|
];
|
|
13863
|
+
/**
|
|
13864
|
+
* The acting user id recorded in a management audit line.
|
|
13865
|
+
*
|
|
13866
|
+
* This is identity, not authorization: the permission decision happened at tool
|
|
13867
|
+
* disclosure, and the backend re-checks the write. Reading `/auth/me` instead of
|
|
13868
|
+
* the full review context keeps an audit write from fetching a permission tree it
|
|
13869
|
+
* does not consult.
|
|
13870
|
+
*/
|
|
13871
|
+
const managementActorId = async () => isTeamDeployment(config) ? (await fetchCaller(host, config)).userId : LOCAL_USER_ID;
|
|
13479
13872
|
const managementHost = {
|
|
13480
13873
|
context: async () => {
|
|
13481
|
-
if (!isTeamDeployment(config)) return
|
|
13482
|
-
userId: "local",
|
|
13483
|
-
username: "local",
|
|
13484
|
-
displayName: "个人用户",
|
|
13485
|
-
primaryRole: "LOCAL_OWNER",
|
|
13486
|
-
roleCodes: ["LOCAL_OWNER"],
|
|
13487
|
-
permissions: [
|
|
13488
|
-
"kb:file:read",
|
|
13489
|
-
"kb:file:write",
|
|
13490
|
-
"kb:file:delete",
|
|
13491
|
-
"kb:file:extract",
|
|
13492
|
-
"kb:audit:read",
|
|
13493
|
-
"kb:grant:manage",
|
|
13494
|
-
"kb:review:decide",
|
|
13495
|
-
"workflow:read",
|
|
13496
|
-
"workflow:publish",
|
|
13497
|
-
"workflow:manage",
|
|
13498
|
-
"workflow:review:decide",
|
|
13499
|
-
"workflow:review:global"
|
|
13500
|
-
],
|
|
13501
|
-
departmentCode: null,
|
|
13502
|
-
departmentPathCodes: [],
|
|
13503
|
-
dataScope: {
|
|
13504
|
-
all: true,
|
|
13505
|
-
self: true,
|
|
13506
|
-
departmentCodes: []
|
|
13507
|
-
},
|
|
13508
|
-
review: {
|
|
13509
|
-
canReview: true,
|
|
13510
|
-
canReviewGlobal: true,
|
|
13511
|
-
canManagePermissions: true,
|
|
13512
|
-
departments: []
|
|
13513
|
-
}
|
|
13514
|
-
};
|
|
13874
|
+
if (!isTeamDeployment(config)) return localManagementContext();
|
|
13515
13875
|
const raw = await request(host, config, "/api/ai/review/context");
|
|
13516
13876
|
const dataScope = raw.dataScope ?? {};
|
|
13517
13877
|
const review = raw.review ?? {};
|
|
@@ -13549,7 +13909,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13549
13909
|
},
|
|
13550
13910
|
kbConfigGet: async () => {
|
|
13551
13911
|
const plan = localConfig(config);
|
|
13552
|
-
const actorId =
|
|
13912
|
+
const actorId = await managementActorId();
|
|
13553
13913
|
const viewDir = viewPathFor({
|
|
13554
13914
|
actualRoot: plan.actualRoot,
|
|
13555
13915
|
viewRoot: plan.viewRoot,
|
|
@@ -13584,7 +13944,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13584
13944
|
if (patch.reviewRequired !== void 0) {
|
|
13585
13945
|
if (typeof patch.reviewRequired !== "boolean") throw new ManagementToolError("invalid", "reviewRequired must be boolean");
|
|
13586
13946
|
const plan = localConfig(config);
|
|
13587
|
-
const actorId =
|
|
13947
|
+
const actorId = await managementActorId();
|
|
13588
13948
|
const targetView = viewPathFor({
|
|
13589
13949
|
actualRoot: plan.actualRoot,
|
|
13590
13950
|
viewRoot: plan.viewRoot,
|
|
@@ -13611,7 +13971,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13611
13971
|
});
|
|
13612
13972
|
await appendManagementAudit(config, host, {
|
|
13613
13973
|
action: "kb_grant_set",
|
|
13614
|
-
actor:
|
|
13974
|
+
actor: await managementActorId(),
|
|
13615
13975
|
detail: input
|
|
13616
13976
|
});
|
|
13617
13977
|
return created;
|
|
@@ -13645,7 +14005,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13645
14005
|
await discardPendingWrite(plan.actualRoot, input.name);
|
|
13646
14006
|
await appendManagementAudit(config, host, {
|
|
13647
14007
|
action: "kb_review_reject",
|
|
13648
|
-
actor:
|
|
14008
|
+
actor: await managementActorId(),
|
|
13649
14009
|
detail: {
|
|
13650
14010
|
name: input.name,
|
|
13651
14011
|
relPath: submission.relPath,
|
|
@@ -13664,7 +14024,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13664
14024
|
await refreshViewIfStale(true, created.relPath);
|
|
13665
14025
|
await appendManagementAudit(config, host, {
|
|
13666
14026
|
action: "kb_review_approve",
|
|
13667
|
-
actor:
|
|
14027
|
+
actor: await managementActorId(),
|
|
13668
14028
|
detail: {
|
|
13669
14029
|
name: input.name,
|
|
13670
14030
|
relPath: submission.relPath,
|
|
@@ -13755,7 +14115,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13755
14115
|
});
|
|
13756
14116
|
await appendManagementAudit(config, host, {
|
|
13757
14117
|
action: "workflow_update",
|
|
13758
|
-
actor:
|
|
14118
|
+
actor: await managementActorId(),
|
|
13759
14119
|
detail: { id }
|
|
13760
14120
|
});
|
|
13761
14121
|
return updated;
|
|
@@ -13765,7 +14125,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
13765
14125
|
const result = await request(host, config, "/api/ai/workflow-publications/" + encodeURIComponent(id), { method: "DELETE" });
|
|
13766
14126
|
await appendManagementAudit(config, host, {
|
|
13767
14127
|
action: "workflow_unpublish",
|
|
13768
|
-
actor:
|
|
14128
|
+
actor: await managementActorId(),
|
|
13769
14129
|
detail: { id }
|
|
13770
14130
|
});
|
|
13771
14131
|
return result;
|
|
@@ -13799,8 +14159,30 @@ function apply(ctx, deploymentConfig) {
|
|
|
13799
14159
|
});
|
|
13800
14160
|
}
|
|
13801
14161
|
};
|
|
13802
|
-
|
|
13803
|
-
|
|
14162
|
+
const disclosure = createToolDisclosure({
|
|
14163
|
+
tools: [
|
|
14164
|
+
workflowPublishTool,
|
|
14165
|
+
...createKnowledgeFileTools(knowledgeToolHost),
|
|
14166
|
+
...createManagementTools(managementHost)
|
|
14167
|
+
],
|
|
14168
|
+
registry: { register: (definition) => host.tools.register(definition) },
|
|
14169
|
+
resolveContext: () => managementHost.context(),
|
|
14170
|
+
log: (message) => host.logger?.warn?.("knowledge-base: " + message)
|
|
14171
|
+
});
|
|
14172
|
+
toolDisclosures.set(ctx, disclosure);
|
|
14173
|
+
toolDisclosure = disclosure;
|
|
14174
|
+
ctx.effect(() => () => disclosure.dispose(), "dsh-knowledge-base: permission-based tool disclosure");
|
|
14175
|
+
if (isTeamDeployment(config)) disclosure.syncToolDisclosure();
|
|
14176
|
+
else disclosure.syncToolDisclosure(localManagementContext());
|
|
14177
|
+
const hostWithEvents = host;
|
|
14178
|
+
if (typeof hostWithEvents.on === "function") ctx.effect(() => {
|
|
14179
|
+
const off = hostWithEvents.on?.("credentials/reference-updated", (ref) => {
|
|
14180
|
+
if (ref === config.credentialRef) disclosure.syncToolDisclosure();
|
|
14181
|
+
});
|
|
14182
|
+
return () => {
|
|
14183
|
+
off?.();
|
|
14184
|
+
};
|
|
14185
|
+
}, "dsh-knowledge-base: tool disclosure refresh on credential change");
|
|
13804
14186
|
ctx.effect(() => startExternalChangeWatcher({
|
|
13805
14187
|
actualRoot: localConfig(config).actualRoot,
|
|
13806
14188
|
log: (message) => host.logger?.warn?.("knowledge-base: " + message),
|
|
@@ -13844,4 +14226,4 @@ async function executeStep(runner, step, hostId, record) {
|
|
|
13844
14226
|
}
|
|
13845
14227
|
}
|
|
13846
14228
|
//#endregion
|
|
13847
|
-
export { Config, KB_ROOT_RESOURCE_ID, KB_SUMMARIZE_SKILL_DESCRIPTION, KB_SUMMARIZE_SKILL_NAME, KB_SUMMARIZE_SKILL_RANK, KB_VECTORIZE_SKILL_DESCRIPTION, KB_VECTORIZE_SKILL_NAME, KB_VECTORIZE_SKILL_RANK, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, LOCAL_USER_ID, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, actualRelPathForView, apply, canManageGrants, canReadPath, canReviewWorkflows, canWritePath, checkLocalPath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, extractionOutcomeOf, fetchCaller, fetchGrants, fetchPrincipals, inject, isTeamDeployment, kbSummarizeSkillPath, kbVectorizeSkillPath, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localStatus, localStorageRoot, mineruUrlFor, name, permissionForKbRoute, planContext, readAuthorized, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, readSharedMineruUrl, resolveMineruUrl, resolveWorkflowActor, revokeGrant, searchKnowledge, syncCallerView, tutorialTemplatePath, withLocalStorageRoot, withStoredStorage };
|
|
14229
|
+
export { Config, KB_ROOT_RESOURCE_ID, KB_SUMMARIZE_SKILL_DESCRIPTION, KB_SUMMARIZE_SKILL_NAME, KB_SUMMARIZE_SKILL_RANK, KB_VECTORIZE_SKILL_DESCRIPTION, KB_VECTORIZE_SKILL_NAME, KB_VECTORIZE_SKILL_RANK, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, LOCAL_USER_ID, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, actualRelPathForView, apply, canManageGrants, canReadPath, canReviewWorkflows, canWritePath, checkLocalPath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, extractionOutcomeOf, fetchCaller, fetchGrants, fetchPrincipals, inject, isTeamDeployment, kbSummarizeSkillPath, kbVectorizeSkillPath, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeBaseToolDisclosure, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localManagementContext, localStatus, localStorageRoot, mineruUrlFor, name, permissionForKbRoute, planContext, readAuthorized, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, readSharedMineruUrl, resolveMineruUrl, resolveWorkflowActor, revokeGrant, searchKnowledge, syncCallerView, tutorialTemplatePath, withLocalStorageRoot, withStoredStorage };
|