@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.
@@ -10,7 +10,9 @@
10
10
  *
11
11
  * It owns no authorization logic. DataOps AuthGuard and PermissionGuard resolve
12
12
  * the caller's roles and grants from the session credential; this plugin only
13
- * forwards requests (AGENTS.md "Managed DataOps Authorization").
13
+ * forwards requests (AGENTS.md "Managed DataOps Authorization"). Which model-facing
14
+ * tools exist is decided from that same permission projection in `toolDisclosure.ts`;
15
+ * path-level checks and the backend remain the enforcement boundary.
14
16
  *
15
17
  * Content retrieval is deliberately absent. Once the view tree is mounted the
16
18
  * official `grep`, `read`, `write`, and `edit` tools operate on it as an
@@ -19,6 +21,8 @@
19
21
  * @module @sparkelf/dsh-knowledge-base
20
22
  */
21
23
  import type { Context } from '@deepseek-ai/cordis';
24
+ import { type ManagementContext } from './managementTools.ts';
25
+ import { type ToolDisclosureCoordinator } from './toolDisclosure.ts';
22
26
  import { type WorkflowActor } from './workflowPublish.ts';
23
27
  import z from '@deepseek-ai/schemastery';
24
28
  import { type KnowledgeConfig, type KnowledgeGrant, type KnowledgeGrantRequest, type KnowledgeRevokeRequest, type KnowledgeSshView, type KnowledgeStatus } from './protocol.ts';
@@ -189,6 +193,18 @@ export declare function isTeamDeployment(config: Config): boolean;
189
193
  * "no user".
190
194
  */
191
195
  export declare const LOCAL_USER_ID = "local";
196
+ /**
197
+ * The permission context a personal deployment discloses tools from.
198
+ *
199
+ * A personal deployment has no accounts, so there is nobody to ask: the local
200
+ * operator owns every capability the product knows about. Kept as a function rather
201
+ * than the inline object inside the management host so the disclosure coordinator
202
+ * can register the personal tool surface synchronously at startup, instead of
203
+ * waiting for a backend round trip that does not exist.
204
+ *
205
+ * @returns the full-capability context a local owner holds.
206
+ */
207
+ export declare function localManagementContext(): ManagementContext;
192
208
  /**
193
209
  * Why a path was refused in a personal deployment.
194
210
  *
@@ -1152,6 +1168,13 @@ export interface SkillCandidateLike {
1152
1168
  export interface SkillDefinitionLike extends SkillCandidateLike {
1153
1169
  readonly content: string;
1154
1170
  }
1171
+ /**
1172
+ * The permission-based tool disclosure coordinator this plugin instance registered.
1173
+ *
1174
+ * @param host - the Host context the plugin was applied to.
1175
+ * @returns the coordinator, or undefined when this context has no knowledge-base plugin.
1176
+ */
1177
+ export declare function knowledgeBaseToolDisclosure(host: object): ToolDisclosureCoordinator | undefined;
1155
1178
  export declare function apply(ctx: Context, deploymentConfig: Config): void;
1156
1179
  /** Re-exported for tests and for callers that need the host shape. */
1157
1180
  export type { RemoteHost, RemoteRunner };
@@ -7,11 +7,14 @@
7
7
  * bypasses all of that, so the model gets tools that go through the same host
8
8
  * operations as the HTTP routes.
9
9
  *
10
- * This module owns argument validation, the permission calls and the tool result
11
- * shape. The host object is supplied by `index.ts`; it is the seam that reuses the
12
- * existing `canReadPath` / `canWritePath` / `listDirectoryWithPermissions` /
13
- * `readKnowledgeFile` / `createKnowledgeEntry` / `moveEntry` implementations, and it
14
- * is what tests replace without importing the whole plugin.
10
+ * This module owns argument validation, path-level permission checks and the tool
11
+ * result shape. Which tools exist is decided once per permission context by
12
+ * `toolDisclosure.ts`; the path checks here decide which data a visible tool may
13
+ * touch and are therefore kept. The host object is supplied by `index.ts`; it is the
14
+ * seam that reuses the existing `canReadPath` / `canWritePath` /
15
+ * `listDirectoryWithPermissions` / `readKnowledgeFile` / `createKnowledgeEntry` /
16
+ * `moveEntry` implementations, and it is what tests replace without importing the
17
+ * whole plugin.
15
18
  *
16
19
  * @module @sparkelf/dsh-knowledge-base/knowledgeTools
17
20
  */
@@ -117,8 +120,6 @@ export type KnowledgeViewSync = {
117
120
  export interface KnowledgeFileToolHost {
118
121
  /** Resolve the caller (team: DataOps `/auth/me`; personal: the local operator). */
119
122
  resolveCaller(): Promise<KnowledgeToolCaller>;
120
- /** Backend DSH permission for this operation, when the host has a permission context. */
121
- hasPermission?(permission: string): Promise<boolean>;
122
123
  /** FTS5 search over the sidecar index, already scoped to the caller. */
123
124
  searchIndex?(input: {
124
125
  query: string;
@@ -231,9 +232,10 @@ export declare const asKnowledgeToolError: (error: unknown, path?: string) => Kn
231
232
  */
232
233
  export declare const assertToolPath: (value: unknown, allowRoot?: boolean) => string;
233
234
  /**
234
- * Build the five file model-facing tools.
235
+ * Build the knowledge-base model-facing tools.
235
236
  *
236
- * Returned definitions are registered by `apply`; tests call the same definitions with
237
- * a fake host to assert permission, path and audit behaviour without a live backend.
237
+ * Returned definitions are registered by `apply` through the permission-based
238
+ * disclosure coordinator; tests call the same definitions with a fake host to assert
239
+ * path, audit and result behaviour without a live backend or a permission backend.
238
240
  */
239
241
  export declare const createKnowledgeFileTools: (host: KnowledgeFileToolHost) => import("@deepseek-ai/dsh-tools").ToolDefinition[];
@@ -2,11 +2,17 @@
2
2
  * Model-facing management tools for the knowledge base and the workflow plaza.
3
3
  *
4
4
  * The UI already exposes these operations. This module gives the model the same
5
- * operations without re-implementing authorization: every tool reads the backend
6
- * review context (roles + department scope) and refuses with a structured 403 before
7
- * touching the host. Writes additionally require an explicit human authorization flag,
8
- * because a model deciding on its own must never be indistinguishable from an operator
9
- * approving a review.
5
+ * operations without re-implementing authorization: which tools exist is decided
6
+ * once from the caller's review context by `toolDisclosure.ts`, so a tool body does
7
+ * not re-read the permission tree on every call. The context is still read where it
8
+ * decides WHICH rows a visible tool may return (department scope, own audit trail)
9
+ * and for the actor recorded in an audit line. Writes additionally require an
10
+ * explicit human authorization flag, because a model deciding on its own must never
11
+ * be indistinguishable from an operator approving a review.
12
+ *
13
+ * DataOps remains the final authority: every host call below reaches a backend
14
+ * route that enforces the JWT's current roles and grants, so a stale disclosure can
15
+ * cause a refusal but never an unauthorized operation.
10
16
  *
11
17
  * Secrets are write-only through the UI; these tools deliberately reject secret-shaped
12
18
  * fields and redact them on reads.
@@ -147,8 +153,6 @@ export declare const DEFAULT_DSH_PERMISSIONS: Readonly<Record<string, readonly s
147
153
  export declare function defaultPermissionsForRoles(roleKeys: readonly string[]): string[];
148
154
  /** True when the caller holds one backend permission key. */
149
155
  export declare function hasPermission(context: ManagementContext, permission: string): boolean;
150
- /** Require one backend permission key; a denial is a structured 403. */
151
- export declare function requirePermission(context: ManagementContext, permission: string, action?: string): void;
152
156
  /** Global administrators: all resources and all review queues. */
153
157
  export declare function isGlobalAdmin(context: ManagementContext): boolean;
154
158
  /** Department reviewers: their department and its descendants. */
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Permission-based disclosure of the model-facing knowledge-base tools.
3
+ *
4
+ * A tool the caller may never use should not be in the model's tool list at all.
5
+ * Registering every tool and refusing inside `execute()` wastes a schema slot,
6
+ * invites a call that will fail, and hides the real capability from the surface
7
+ * that decides what to offer. Disclosure is therefore decided ONCE per permission
8
+ * context: the coordinator registers the tools a context allows and disposes the
9
+ * rest. Execution never re-reads the full permission tree; the backend remains the
10
+ * final authority for every request the tools then make.
11
+ *
12
+ * ## What is decided where
13
+ *
14
+ * - **Disclosure (this module).** A tool's group and its required permission keys
15
+ * or role capability. Evaluated from the caller's `/api/ai/review/context`
16
+ * projection (`ManagementContext`), the same object the Settings panel and route
17
+ * gate already read.
18
+ * - **Data filtering (the tools themselves).** Path-level `canRead` / `canWrite`
19
+ * calls stay in `execute()`: they decide WHICH rows a visible tool may touch, not
20
+ * whether the tool exists. The same holds for department-scope filtering of
21
+ * grants, audit entries and review queues.
22
+ * - **Final authorization (DataOps).** Every backend route the tools call enforces
23
+ * the JWT's current roles and grants. A stale tool registration can therefore
24
+ * cause a refusal, never an unauthorized read or write.
25
+ *
26
+ * ## Groups
27
+ *
28
+ * `read` tools exist for a caller with the knowledge-base read key; `write` tools
29
+ * for the editors; `manage` for the administrator/reviewer surfaces; `publish` for
30
+ * workflow publication. Groups exist so a permission change can be applied as a
31
+ * set, and so a fault in one group is reflected in one place rather than in every
32
+ * tool body.
33
+ *
34
+ * @module @sparkelf/dsh-knowledge-base/toolDisclosure
35
+ */
36
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
37
+ import { type ManagementContext } from './managementTools.ts';
38
+ /** The four disclosure groups a knowledge-base tool can belong to. */
39
+ export declare const TOOL_GROUPS: readonly ["read", "write", "manage", "publish"];
40
+ /** One disclosure group. */
41
+ export type KnowledgeToolGroup = (typeof TOOL_GROUPS)[number];
42
+ /**
43
+ * One tool's disclosure rule.
44
+ *
45
+ * `permissions` lists keys that must ALL be held. `capability` names a role
46
+ * capability that must also be present; it is how the tools whose product
47
+ * description is "reviewer only" or "global admin only" are filtered without
48
+ * inventing a permission key the backend does not issue.
49
+ */
50
+ export interface ToolAccessRule {
51
+ /** Which group the tool is registered and disposed with. */
52
+ readonly group: KnowledgeToolGroup;
53
+ /** Every permission key the caller must hold. */
54
+ readonly permissions?: readonly string[];
55
+ /** Reviewer/admin capability the caller must have, in addition to permissions. */
56
+ readonly capability?: 'reviewer' | 'admin';
57
+ }
58
+ /**
59
+ * Disclosure rules for every model-facing tool this package registers.
60
+ *
61
+ * The map is the single list of what exists; the coordinator rejects a tool
62
+ * definition without a rule and a rule without a definition at construction time,
63
+ * so a new tool cannot ship undisclosed or leave a stale policy behind.
64
+ */
65
+ export declare const KB_TOOL_ACCESS: Readonly<Record<string, ToolAccessRule>>;
66
+ /** Tool names per group, derived from {@link KB_TOOL_ACCESS}. */
67
+ export declare const TOOL_GROUPS_OF: Readonly<Record<KnowledgeToolGroup, readonly string[]>>;
68
+ /** Every model-facing tool name this package discloses. */
69
+ export declare const KNOWLEDGE_TOOL_NAMES: readonly string[];
70
+ /** One tool's disclosure decision. */
71
+ export interface ToolAccessDecision {
72
+ /** Whether the tool may be registered for this context. */
73
+ readonly allowed: boolean;
74
+ /** The tool's group, when a rule exists. */
75
+ readonly group?: KnowledgeToolGroup;
76
+ /**
77
+ * What the context lacks, as permission keys (`kb:file:write`) or capability
78
+ * labels (`reviewer`, `admin`, `permission-context`). Empty when allowed.
79
+ */
80
+ readonly missing: readonly string[];
81
+ }
82
+ /**
83
+ * Decide whether one tool is visible for a permission context.
84
+ *
85
+ * An absent context is a denial: disclosure happens before execution, so an
86
+ * unresolved permission tree must withhold the surface rather than expose it.
87
+ * Unknown tool names are denied too; the coordinator rejects them at construction,
88
+ * and this function keeps the same fail-closed answer if one reaches it anyway.
89
+ *
90
+ * @param name - model-facing tool name.
91
+ * @param context - the caller's review context, or undefined while it is unavailable.
92
+ * @returns the decision and the requirements the context did not satisfy.
93
+ */
94
+ export declare function decideToolAccess(name: string, context: ManagementContext | undefined): ToolAccessDecision;
95
+ /** One withheld tool and the requirements that withheld it. */
96
+ export interface ToolDenial {
97
+ readonly name: string;
98
+ readonly group?: KnowledgeToolGroup;
99
+ readonly missing: readonly string[];
100
+ }
101
+ /** The disclosure state after one pass. */
102
+ export interface ToolDisclosureSnapshot {
103
+ /** Tool names registered after this pass, in definition order. */
104
+ readonly registered: readonly string[];
105
+ /** Tools withheld from the model, with the unsatisfied requirement per tool. */
106
+ readonly denied: readonly ToolDenial[];
107
+ /** Groups that currently have at least one registered tool. */
108
+ readonly activeGroups: readonly KnowledgeToolGroup[];
109
+ /** Set when the permission context could not be resolved; every tool is disposed. */
110
+ readonly error?: string;
111
+ }
112
+ /** The tool registry seam this coordinator needs. */
113
+ export interface ToolRegistryLike {
114
+ /** Register one tool; returns the exact disposer that unregisters it. */
115
+ register(definition: ToolDefinition): () => void;
116
+ }
117
+ /** Construction options for a {@link ToolDisclosureCoordinator}. */
118
+ export interface ToolDisclosureOptions {
119
+ /** Every model-facing tool this plugin can register. */
120
+ readonly tools: readonly ToolDefinition[];
121
+ /** The host registry to register into and dispose from. */
122
+ readonly registry: ToolRegistryLike;
123
+ /**
124
+ * Resolve the caller's current review context. Called on an explicit refresh,
125
+ * never from a tool's `execute()`.
126
+ */
127
+ readonly resolveContext: () => Promise<ManagementContext>;
128
+ /** Diagnostic sink; a failed refresh is reported here instead of thrown. */
129
+ readonly log?: (message: string) => void;
130
+ }
131
+ /**
132
+ * Register and dispose knowledge-base tools as the caller's permission context changes.
133
+ *
134
+ * The coordinator owns every tool registration. A pass computes one decision per
135
+ * tool, registers the newly allowed ones, disposes the newly denied ones and
136
+ * leaves unchanged tools untouched, so an unchanged context causes no registry
137
+ * churn. Concurrent refreshes share one in-flight context read; a pass that is
138
+ * superseded by a newer context never applies its stale answer.
139
+ *
140
+ * On a context-resolution failure every gated tool is disposed. That is the
141
+ * fail-closed direction: an unavailable permission tree cannot prove a capability,
142
+ * and a tool surface that outlives its proof is exactly the leak disclosure exists
143
+ * to prevent.
144
+ */
145
+ export declare class ToolDisclosureCoordinator {
146
+ private readonly definitions;
147
+ /** Registered tools and their exact registry disposers. */
148
+ private readonly disposers;
149
+ private readonly options;
150
+ /** Monotonic pass counter; a slower older read must not overwrite a newer one. */
151
+ private generation;
152
+ private inFlight?;
153
+ private disposed;
154
+ private snapshot?;
155
+ constructor(options: ToolDisclosureOptions);
156
+ /** Tool names currently registered, in definition order. */
157
+ get registeredTools(): readonly string[];
158
+ /** The most recent disclosure pass, if one has completed. */
159
+ get lastSnapshot(): ToolDisclosureSnapshot | undefined;
160
+ /**
161
+ * Recompute the tool surface.
162
+ *
163
+ * @param context - a context already read by the caller (a route gate or status
164
+ * request). Passing it avoids a second backend round trip. Omit it to resolve
165
+ * one through {@link ToolDisclosureOptions.resolveContext}.
166
+ * @returns the resulting snapshot; a context failure is reported in `error`
167
+ * rather than thrown, because a refresh must never fail the request that
168
+ * triggered it.
169
+ */
170
+ syncToolDisclosure(context?: ManagementContext): Promise<ToolDisclosureSnapshot>;
171
+ /** Dispose every tool this coordinator registered; later refreshes are no-ops. */
172
+ dispose(): void;
173
+ /** Register allowed tools and dispose denied ones for one resolved context. */
174
+ private apply;
175
+ /** Dispose everything and report why the surface is empty. */
176
+ private fail;
177
+ /** Dispose every registered tool; safe to call more than once. */
178
+ private disposeRegistered;
179
+ /** Snapshot for a pass that cannot run at all. */
180
+ private emptySnapshot;
181
+ }
182
+ /**
183
+ * Build a coordinator.
184
+ *
185
+ * @param options - the definitions, registry, context resolver and log sink.
186
+ * @returns the coordinator the plugin stores and refreshes.
187
+ */
188
+ export declare function createToolDisclosure(options: ToolDisclosureOptions): ToolDisclosureCoordinator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkelf/dsh-knowledge-base",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "DataOps-backed knowledge base for DeepSeek Harness: a left-sidebar destination, Settings configuration, and index tooling over a mounted NAS view.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -48,6 +48,11 @@
48
48
  "skills/**/SKILL.md",
49
49
  "skills/**/i18n/*.json"
50
50
  ],
51
+ "scripts": {
52
+ "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
53
+ "bundle": "tsdown",
54
+ "prepack": "pnpm run build"
55
+ },
51
56
  "engines": {
52
57
  "node": "^22.19.0 || >=24.0.0"
53
58
  },
@@ -103,9 +108,5 @@
103
108
  "dsh-better-sidebar": {
104
109
  "optional": true
105
110
  }
106
- },
107
- "scripts": {
108
- "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
109
- "bundle": "tsdown"
110
111
  }
111
- }
112
+ }