dsh-plugin-inspector 0.6.0 → 0.8.0

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/lib/knowledge.js CHANGED
@@ -11,9 +11,17 @@
11
11
  */
12
12
  /**
13
13
  * Harness version these tables were transcribed from — the version string in
14
- * the checkout's own `packages/bundle/*/package.json`.
14
+ * the shipped bundles' own `package.json`, which is `dsh`'s own version.
15
+ *
16
+ * Re-verified against `0.1.2-rc.1`, the release npm tags `latest`, by
17
+ * extracting each table from the published packages and diffing it against the
18
+ * one here. What moved: thirteen row ids the bundles gained against three they
19
+ * dropped, four rows the base layer inserts that only the web bundle carried,
20
+ * eleven seam keys against one dropped, and two waterfall events against one
21
+ * dropped. What did not: the sandbox trap table, the teardown surfaces, and
22
+ * every `DECISION_EVENT_DEFAULTS` citation, all re-read at this release.
15
23
  */
16
- export const HARNESS_REFERENCE = '0.1.0-rc.5';
24
+ export const HARNESS_REFERENCE = '0.1.2-rc.1';
17
25
  /**
18
26
  * The three profile bundles the harness ships, mapped to what each one is.
19
27
  * A package that *is* one of these composes the core rows rather than modifying
@@ -45,13 +53,11 @@ export const CORE_ROWS = new Map([
45
53
  ['agent-instructions', { module: '@deepseek-ai/dsh-agent-instructions', bundles: ['base'] }],
46
54
  ['agent-loop', { module: '@deepseek-ai/dsh-agent-loop', bundles: ['base'] }],
47
55
  ['agent-presets', { module: '@deepseek-ai/dsh-agent-presets', bundles: ['web-app'] }],
48
- ['api-gateway', { module: '@deepseek-ai/dsh-host-apiproxy', bundles: ['web-app'] }],
49
56
  ['api-remotes', { module: '@deepseek-ai/dsh-api-remotes', bundles: ['web-app'] }],
50
57
  ['approval', { module: '@deepseek-ai/dsh-user-approval', bundles: ['base'] }],
51
58
  ['attachment-local', { module: '@deepseek-ai/dsh-attachment-local', bundles: ['base'] }],
52
59
  ['bash-sandbox', { module: '@deepseek-ai/dsh-bash-sandbox', bundles: ['base'] }],
53
60
  ['client-hmr', { module: '@deepseek-ai/dsh-client-hmr', bundles: ['web-app'] }],
54
- ['client-runtime', { module: '@deepseek-ai/dsh-client-runtime', bundles: ['web-app'] }],
55
61
  ['code-runtime', { module: '@deepseek-ai/dsh-code-runtime-worker-thread', bundles: ['headless', 'web-app'] }],
56
62
  ['command-compact', { module: '@deepseek-ai/dsh-command-compact', bundles: ['base'] }],
57
63
  ['command-feedback', { module: '@deepseek-ai/dsh-command-feedback', bundles: ['base'] }],
@@ -62,7 +68,9 @@ export const CORE_ROWS = new Map([
62
68
  ['cordis-client-runner', { module: '@deepseek-ai/dsh-cordis-client-runner', bundles: ['web-app'] }],
63
69
  ['cordis-host-runner', { module: '@deepseek-ai/dsh-cordis-host-runner', bundles: ['web-app'] }],
64
70
  ['credentials', { module: '@deepseek-ai/dsh-credentials-local', bundles: ['base'] }],
71
+ ['deepseek-llm-api-extensions', { module: '@deepseek-ai/dsh-deepseek-llm-api-extensions', bundles: ['base'] }],
65
72
  ['directory-picker', { module: '@deepseek-ai/dsh-host-directory-picker-auto', bundles: ['web-app'] }],
73
+ ['file-reference-local', { module: '@deepseek-ai/dsh-file-reference-local', bundles: ['web-app'] }],
66
74
  ['fs-observation-policy', { module: '@deepseek-ai/dsh-fs-observation-policy', bundles: ['base'] }],
67
75
  ['fs-sandbox', { module: '@deepseek-ai/dsh-fs-sandbox', bundles: ['base'] }],
68
76
  ['goal', { module: '@deepseek-ai/dsh-goal', bundles: ['base'] }],
@@ -81,33 +89,40 @@ export const CORE_ROWS = new Map([
81
89
  ['permission', { module: '@deepseek-ai/dsh-permission-presets', bundles: ['base'] }],
82
90
  ['plan-mode', { module: '@deepseek-ai/dsh-plan-mode', bundles: ['base'] }],
83
91
  ['plugin-inventory', { module: '@deepseek-ai/dsh-host-plugin-inventory', bundles: ['web-app'] }],
92
+ ['plugin-package-inventory-deepseek', { module: '@deepseek-ai/dsh-plugin-package-inventory-deepseek', bundles: ['base'] }],
84
93
  ['pwsh-sandbox', { module: '@deepseek-ai/dsh-pwsh-sandbox', bundles: ['base'] }],
85
94
  ['repeat-tool-reminder', { module: '@deepseek-ai/dsh-repeat-tool-reminder', bundles: ['base'] }],
86
95
  ['sandbox', { module: '@deepseek-ai/dsh-sandbox-local', bundles: ['base'] }],
87
96
  ['sandbox-policy', { module: '@deepseek-ai/dsh-sandbox-policy', bundles: ['base'] }],
88
97
  ['session', { module: '@deepseek-ai/dsh-session', bundles: ['base'] }],
89
98
  ['session-checkpoint-policy', { module: '@deepseek-ai/dsh-session-checkpoint-policy', bundles: ['base'] }],
99
+ ['session-controller', { module: '@deepseek-ai/dsh-api-session-controller', bundles: ['web-app'] }],
100
+ ['session-log-deepseek', { module: '@deepseek-ai/dsh-session-log-deepseek', bundles: ['base'] }],
90
101
  ['session-log-download', { module: '@deepseek-ai/dsh-session-log-export', bundles: ['web-app'] }],
91
102
  ['session-persistence-jsonl', { module: '@deepseek-ai/dsh-session-persistence-jsonl', bundles: ['base'] }],
92
103
  ['session-projection', { module: '@deepseek-ai/dsh-session-projection', bundles: ['base'] }],
93
- ['session-projection-cache', { module: '@deepseek-ai/dsh-session-projection-cache', bundles: ['web-app'] }],
104
+ ['session-projection-cache', { module: '@deepseek-ai/dsh-session-projection-cache', bundles: ['base'] }],
94
105
  ['session-query-sqlite', { module: '@deepseek-ai/dsh-session-query-sqlite', bundles: ['base'] }],
106
+ ['session-reference', { module: '@deepseek-ai/dsh-session-reference', bundles: ['web-app'] }],
95
107
  ['session-stats', { module: '@deepseek-ai/dsh-session-stats', bundles: ['web-app'] }],
96
108
  ['session-telemetry-otel', { module: '@deepseek-ai/dsh-session-telemetry-otel', bundles: ['base'] }],
97
109
  ['session-title', { module: '@deepseek-ai/dsh-session-title', bundles: ['base'] }],
98
110
  ['session-title-llm', { module: '@deepseek-ai/dsh-session-title-first-prompt-llm', bundles: ['base'] }],
111
+ ['session-turn-outline', { module: '@deepseek-ai/dsh-session-turn-outline', bundles: ['web-app'] }],
99
112
  ['settings', { module: '@deepseek-ai/dsh-settings-file', bundles: ['base'] }],
113
+ ['settings-controller', { module: '@deepseek-ai/dsh-api-settings-controller', bundles: ['web-app'] }],
100
114
  ['shell-env', { module: '@deepseek-ai/dsh-shell-env', bundles: ['base'] }],
101
115
  ['skill', { module: '@deepseek-ai/dsh-skill', bundles: ['base'] }],
102
116
  ['skill-badge', { module: '@deepseek-ai/dsh-skill-badge', bundles: ['base'] }],
103
117
  ['skill-filesystem', { module: '@deepseek-ai/dsh-skill-filesystem', bundles: ['base'] }],
104
118
  ['spill-local', { module: '@deepseek-ai/dsh-spill-local', bundles: ['base'] }],
105
119
  ['spill-policy', { module: '@deepseek-ai/dsh-spill-policy', bundles: ['base'] }],
106
- ['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['web-app'] }],
107
- ['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['web-app'] }],
108
- ['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['web-app'] }],
120
+ ['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['base'] }],
121
+ ['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['base'] }],
122
+ ['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['base'] }],
109
123
  ['subagent', { module: '@deepseek-ai/dsh-subagent', bundles: ['base'] }],
110
124
  ['subagent-fork-in-process', { module: '@deepseek-ai/dsh-subagent-fork-in-process', bundles: ['base'] }],
125
+ ['subagent-model-selection-settings', { module: '@deepseek-ai/dsh-tool-subagent/model-selection-settings', bundles: ['web-app'] }],
111
126
  ['subagent-spawn-in-process', { module: '@deepseek-ai/dsh-subagent-spawn-in-process', bundles: ['base'] }],
112
127
  ['subprocess', { module: '@deepseek-ai/dsh-subprocess-local', bundles: ['base'] }],
113
128
  ['system-prompt', { module: '@deepseek-ai/dsh-system-prompt', bundles: ['base'] }],
@@ -128,7 +143,6 @@ export const CORE_ROWS = new Map([
128
143
  ['tool-subagent-control', { module: '@deepseek-ai/dsh-tool-subagent-control', bundles: ['base'] }],
129
144
  ['tool-subagent-fork', { module: '@deepseek-ai/dsh-tool-subagent', bundles: ['base'] }],
130
145
  ['tool-subagent-list-agents', { module: '@deepseek-ai/dsh-tool-subagent-control/list-agents', bundles: ['base'] }],
131
- ['tool-subagent-report', { module: '@deepseek-ai/dsh-tool-subagent-report', bundles: ['base'] }],
132
146
  ['tool-todo', { module: '@deepseek-ai/dsh-tool-todo', bundles: ['base'] }],
133
147
  ['tool-web', { module: '@deepseek-ai/dsh-tool-web', bundles: ['base'] }],
134
148
  ['tool-workflow', { module: '@deepseek-ai/dsh-tool-workflow', bundles: ['base'] }],
@@ -137,6 +151,10 @@ export const CORE_ROWS = new Map([
137
151
  ['typert-gateway', { module: '@deepseek-ai/dsh-api-gateway', bundles: ['base'] }],
138
152
  ['typert-loader', { module: '@deepseek-ai/dsh-typert-loader', bundles: ['base'] }],
139
153
  ['ui-agent-preset', { module: '@deepseek-ai/dsh-client-ui-agent-preset', bundles: ['web-app'] }],
154
+ ['ui-approval', { module: '@deepseek-ai/dsh-client-ui-approval', bundles: ['web-app'] }],
155
+ ['ui-attachment', { module: '@deepseek-ai/dsh-client-ui-attachment', bundles: ['web-app'] }],
156
+ ['ui-brand-official', { module: '@deepseek-ai/dsh-client-ui-brand-official', bundles: ['web-app'] }],
157
+ ['ui-chat', { module: '@deepseek-ai/dsh-client-ui-chat', bundles: ['web-app'] }],
140
158
  ['ui-commands', { module: '@deepseek-ai/dsh-client-ui-commands', bundles: ['web-app'] }],
141
159
  ['ui-conversation', { module: '@deepseek-ai/dsh-client-ui-conversation', bundles: ['web-app'] }],
142
160
  ['ui-cordis', { module: '@deepseek-ai/dsh-client-ui-cordis', bundles: ['web-app'] }],
@@ -149,6 +167,10 @@ export const CORE_ROWS = new Map([
149
167
  ['ui-model-selection', { module: '@deepseek-ai/dsh-client-ui-model-selection', bundles: ['web-app'] }],
150
168
  ['ui-permission', { module: '@deepseek-ai/dsh-client-ui-permission-presets', bundles: ['web-app'] }],
151
169
  ['ui-plan', { module: '@deepseek-ai/dsh-client-ui-plan', bundles: ['web-app'] }],
170
+ ['ui-reference', { module: '@deepseek-ai/dsh-client-ui-reference', bundles: ['web-app'] }],
171
+ ['ui-renderer', { module: '@deepseek-ai/dsh-client-ui-renderer', bundles: ['web-app'] }],
172
+ ['ui-schedule', { module: '@deepseek-ai/dsh-client-ui-schedule', bundles: ['web-app'] }],
173
+ ['ui-session', { module: '@deepseek-ai/dsh-client-ui-session', bundles: ['web-app'] }],
152
174
  ['ui-settings', { module: '@deepseek-ai/dsh-client-ui-settings', bundles: ['web-app'] }],
153
175
  ['ui-settings-general', { module: '@deepseek-ai/dsh-client-ui-settings-general', bundles: ['web-app'] }],
154
176
  ['ui-settings-models', { module: '@deepseek-ai/dsh-client-ui-settings-models', bundles: ['web-app'] }],
@@ -165,12 +187,14 @@ export const CORE_ROWS = new Map([
165
187
  ['ui-workspace', { module: '@deepseek-ai/dsh-client-ui-workspace', bundles: ['web-app'] }],
166
188
  ['user-questions', { module: '@deepseek-ai/dsh-user-questions', bundles: ['base'] }],
167
189
  ['web', { module: '@deepseek-ai/dsh-web', bundles: ['base'] }],
190
+ ['web-fetch-http', { module: '@deepseek-ai/dsh-web-fetch-http', bundles: ['base'] }],
168
191
  ['web-runtime', { module: '@deepseek-ai/dsh-web-app', bundles: ['web-app'] }],
169
192
  ['web-search-deepseek', { module: '@deepseek-ai/dsh-web-search-deepseek', bundles: ['base'] }],
170
193
  ['web-startup', { module: '@deepseek-ai/dsh-web-app/startup', bundles: ['web-app'] }],
171
194
  ['webserver', { module: '@deepseek-ai/dsh-host-webserver', bundles: ['web-app'] }],
172
195
  ['workflow-worker-thread', { module: '@deepseek-ai/dsh-workflow-worker-thread', bundles: ['base'] }],
173
196
  ['workspace', { module: '@deepseek-ai/dsh-workspace', bundles: ['web-app'] }],
197
+ ['workspace-controller', { module: '@deepseek-ai/dsh-api-workspace-controller', bundles: ['web-app'] }],
174
198
  ]);
175
199
  /** Row ids the shipped bundles define. */
176
200
  export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
@@ -207,21 +231,116 @@ export const SECURITY_ROW_IDS = new Map([
207
231
  * replaces a core service for every consumer in its scope.
208
232
  */
209
233
  export const SEAM_KEYS = new Set([
210
- 'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'apiProxy', 'approval',
211
- 'attachments', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
212
- 'directoryPicker', 'e2b', 'fs', 'goals', 'invariants', 'jobs', 'llm', 'lsp',
213
- 'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy',
214
- 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections', 'sessionQuery',
215
- 'sessionReferenceResolver', 'sessions', 'sessionTelemetry', 'sessionTitle', 'settings',
216
- 'shell', 'shellEnv', 'skills', 'spillStore', 'storage', 'storageDomain', 'subagents',
217
- 'subprocess', 'systemPrompt', 'terminals', 'timer', 'tokenMeter', 'toolResultPruner',
218
- 'tools', 'typert', 'typertGateway', 'userQuestions', 'web', 'webServer', 'workflowEngine',
219
- 'workspaceRegistry',
234
+ 'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'approval', 'attachments',
235
+ 'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
236
+ 'credentialsController', 'deepseekLlmApiExtensions', 'directoryPicker', 'directoryPickerController',
237
+ 'e2b', 'fileReferences', 'fs', 'goals', 'inspector', 'invariants', 'jobs', 'llm', 'lsp',
238
+ 'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy', 'sessionController',
239
+ 'sessionFileReferences', 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections',
240
+ 'sessionQuery', 'sessionReferenceResolver', 'sessions', 'sessionSkillCatalog', 'sessionTelemetry',
241
+ 'sessionTitle', 'settings', 'settingsController', 'shell', 'shellEnv', 'skills', 'spillStore', 'storage',
242
+ 'storageDomain', 'subagentModelSelection', 'subagents', 'subprocess', 'systemPrompt', 'terminals',
243
+ 'timer', 'tokenMeter', 'toolResultPruner', 'tools', 'typert', 'typertGateway', 'userQuestions', 'web',
244
+ 'webhookRuntime', 'webServer', 'workflowEngine', 'workspaceController', 'workspaceRegistry',
220
245
  ]);
221
- /** The subset of {@link SEAM_KEYS} whose replacement removes a constraint. */
246
+ /**
247
+ * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
248
+ *
249
+ * `authorization` is the registry of flows that obtain a credential through a
250
+ * conversation with the user, so providing it means owning that conversation.
251
+ * That is the same class of substitution as `credentials`, which this set
252
+ * already holds. `fileReferences` decides which paths are offered for
253
+ * completion and `agentTeams` is the team form of `subagents`, which is
254
+ * deliberately not here either, so neither of those is in this set.
255
+ *
256
+ * Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
257
+ * services that own one `ctx.remote.*` namespace the browser client calls
258
+ * across the wire. A controller belongs here only when the traffic a
259
+ * substitution redirects to it carries a secret, an execution boundary, or a
260
+ * decision. A controller that forwards its seam's own verbs and adds a wire
261
+ * failure vocabulary does not, because the seam it fronts is reachable from
262
+ * `ctx` without substituting anything.
263
+ *
264
+ * Included:
265
+ * - `credentialsController` is what a browser configuration page calls to store
266
+ * a credential. `set(ref, value)` receives the plaintext secret and hands it
267
+ * to `ctx.credentials`
268
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
269
+ * `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
270
+ * answer to the three fields `CredentialInfo` declares. A layer that provides
271
+ * it takes both halves: every secret typed into the settings page, and the
272
+ * freedom to answer a read with the stored value.
273
+ * - `settingsController` passes `redactSecrets: true` on every remote read
274
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
275
+ * which is what keeps a `role('secret')` field out of a settings response;
276
+ * `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
277
+ * `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
278
+ * value in plaintext from the configuration page. Providing it puts the layer
279
+ * on both directions of a secret's path.
280
+ * - `sessionController` resolves each new Session's cwd from the wire request
281
+ * and hands it to `ensureSession`
282
+ * (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
283
+ * `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
284
+ * `workspace-write` root the enforcing filesystem, bash and terminal backends
285
+ * fence against. Its `prompt` verb builds the message admitted to the agent
286
+ * under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
287
+ * sandbox root for every session created from the client, and the text that
288
+ * reaches the model under the user's own source label.
289
+ * - `webhookRuntime` is what a provider adapter such as
290
+ * `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
291
+ * one built-in action creates a Session from a rule result whose fields
292
+ * include `workspacePath`, `permissionPreset` and `prompt`
293
+ * (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
294
+ * `createWebhookSession` applies that preset through
295
+ * `ctx.permissionPresets.set` before admitting the prompt
296
+ * (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
297
+ * Providing it picks the approval and sandbox preset for an agent started by
298
+ * a remote delivery with no user present.
299
+ *
300
+ * Excluded:
301
+ * - `workspaceController` forwards `request.path` to
302
+ * `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
303
+ * error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
304
+ * to `:212`). The registry it fronts is `workspaceRegistry`, which is not
305
+ * here, so the substitution reaches nothing the seam does not already offer.
306
+ * - `directoryPickerController` is three delegations to
307
+ * `ctx.directoryPicker.capability()` behind a check that refuses a verb the
308
+ * composed backend does not serve
309
+ * (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
310
+ * Path fencing lives in the backend, and `directoryPicker` is not here.
311
+ * - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
312
+ * is excluded above for the same reason: the traffic is path candidates
313
+ * offered for completion.
314
+ * - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
315
+ * for one Session's human-facing composer
316
+ * (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
317
+ * `:227`), and its only consumer in this release is the client skill picker
318
+ * (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
319
+ * model through `ctx.skills.list()` in
320
+ * `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
321
+ * - `subagentModelSelection` is a settings owner answering `{ enabled,
322
+ * allowedModels }`, sampled when an Agent receives its delegation tools. Model
323
+ * routing is `llm` and delegation is `subagents`; neither is here.
324
+ * - `deepseekLlmApiExtensions` hands a substitute the serialized request body
325
+ * and merges the fields it returns, but
326
+ * `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
327
+ * field colliding with the base request, so `messages`, `tools` and `model`
328
+ * are not writable through it. The constraint is in the adapter, not the
329
+ * registry the substitution replaces.
330
+ * - `inspector` is declared by the catalogue and implemented by no shipped
331
+ * package: in `0.1.2-rc.1` the key, `InspectorJsonValue` and
332
+ * `CordisRuntimeTreeReader` occur only in
333
+ * `@deepseek-ai/dsh-tool-cordis/lib/index.js`, and nothing provides or reads
334
+ * `ctx.inspector`. No composed row enforces anything through it, so a
335
+ * substitution displaces nothing. This is the one entry decided from the
336
+ * catalogue rather than from an implementation; a release that ships one is a
337
+ * reason to decide it again.
338
+ */
222
339
  export const SECURITY_SEAM_KEYS = new Set([
223
- 'approval', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials', 'subprocess',
224
- 'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry', 'invariants',
340
+ 'approval', 'authorization', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
341
+ 'credentialsController', 'settingsController', 'sessionController', 'webhookRuntime',
342
+ 'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry',
343
+ 'invariants',
225
344
  ]);
226
345
  /**
227
346
  * Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
@@ -229,16 +348,32 @@ export const SECURITY_SEAM_KEYS = new Set([
229
348
  * without calling it short-circuits the chain including the built-in behavior.
230
349
  *
231
350
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
351
+ *
352
+ * `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
353
+ * and adds `user-questions/request`. Both replace content in a durable log copy
354
+ * or answer a pending request; neither is an `emit` event, so both hand a
355
+ * listener the trailing `next`.
232
356
  */
233
357
  export const WATERFALL_EVENTS = new Set([
234
358
  'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
235
359
  'fs/edit-intent', 'fs/write-intent', 'llm/stream', 'session-telemetry/record',
236
- 'system-prompt/assemble', 'tools/code-dispatch-log', 'tools/execute',
237
- 'tools/post-execute', 'tools/pre-execute',
360
+ 'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
361
+ 'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
238
362
  ]);
239
- /** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
363
+ /**
364
+ * Waterfall events whose short-circuit removes a decision the user would
365
+ * otherwise make.
366
+ *
367
+ * `user-questions/request` is here for the same reason `approval/request` is:
368
+ * `ctx.userQuestions` pauses a tool call until a human answers, and the
369
+ * answerers that put the question on a screen are listeners in the chain rather
370
+ * than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
371
+ * A listener that returns an answer without calling `next()` answers on the
372
+ * user's behalf and the question is never shown.
373
+ */
240
374
  export const DECISION_EVENTS = new Set([
241
375
  'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
376
+ 'user-questions/request',
242
377
  ]);
243
378
  /**
244
379
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -252,6 +387,8 @@ export const SANDBOX_DENIED_GLOBALS = new Map([
252
387
  ['setTimeout', "redirected to the cordis timer service (inject: ['timer'])"],
253
388
  ['setInterval', "redirected to the cordis timer service (inject: ['timer'])"],
254
389
  ['setImmediate', "redirected to the cordis timer service (inject: ['timer'])"],
390
+ ['clearTimeout', "redirected to the cordis timer service (inject: ['timer'])"],
391
+ ['clearInterval', "redirected to the cordis timer service (inject: ['timer'])"],
255
392
  ]);
256
393
  /**
257
394
  * Node builtins that start or evaluate code off the mediated path. A mounted
@@ -389,3 +526,130 @@ export const HARNESS_INERT_CALLS = new Set([
389
526
  * in code, and it is plain YAML.
390
527
  */
391
528
  export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
529
+ /**
530
+ * How a Cordis waterfall listener delegates, and what happens when it does not.
531
+ *
532
+ * Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
533
+ * `lib/index.js:317-327`:
534
+ *
535
+ * ```js
536
+ * waterfall(...args) {
537
+ * const cbs = this.dispatch("waterfall", args);
538
+ * const inner = args.pop();
539
+ * const next = () => { return (cbs.shift() ?? inner)(...args); };
540
+ * args.push(next);
541
+ * return next();
542
+ * }
543
+ * ```
544
+ *
545
+ * `next` is the trailing argument every listener receives, and `inner` is the
546
+ * harness's own built-in behavior. A listener that returns without calling
547
+ * `next()` therefore ends the chain: neither the listeners still in `cbs` nor
548
+ * `inner` run.
549
+ *
550
+ * The scope of that is one dispatch, not the registry. `dispatch()` builds
551
+ * `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
552
+ * never touched and every skipped listener is registered and runs normally on
553
+ * the next dispatch. The precise word is veto, not removal — Cordis's own
554
+ * JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
555
+ * the built-in behavior". Removal is a different capability with a different
556
+ * reach, and it has its own table below.
557
+ */
558
+ export const WATERFALL_NEXT_PARAMETER = 'next';
559
+ /**
560
+ * What each decision waterfall's built-in `next` settles on when no listener
561
+ * claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
562
+ *
563
+ * This is what a listener that never calls `next()` replaces. The inner
564
+ * callback is the last argument at each site:
565
+ * - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
566
+ * `() => Promise.resolve({ kind: "allow" })`
567
+ * - `tools/execute` — `dsh-tools/lib/index.js:3214`,
568
+ * `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
569
+ * body of the tool call itself
570
+ * - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
571
+ * `() => Promise.resolve("unavailable")`, and the surface that would ask the
572
+ * user is one of the listeners in the chain rather than the inner callback
573
+ * - `user-questions/request` —
574
+ * `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
575
+ * callback passed at `:69`, which rejects with a `UserQuestionError` carrying
576
+ * code `NO_PROVIDER`
577
+ *
578
+ * The three tables in this module that name events (`WATERFALL_EVENTS`,
579
+ * `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
580
+ */
581
+ export const DECISION_EVENT_DEFAULTS = new Map([
582
+ ['approval/request', 'the request falls through to `"unavailable"` only after every composed answerer — '
583
+ + 'including the surface that would ask the user — has had the dispatch'],
584
+ ['tools/pre-execute', 'the gate settles on `{ kind: "allow" }` after every other listener, and only then are '
585
+ + '`ctx.tools.guard()` denials consulted'],
586
+ ['tools/execute', 'the tool body itself runs'],
587
+ ['fs/write-intent', 'the write intent reaches the policy rows that decide it'],
588
+ ['fs/edit-intent', 'the edit intent reaches the policy rows that decide it'],
589
+ ['user-questions/request', 'the request rejects with `NO_PROVIDER` only after every composed answerer — '
590
+ + 'including the one that puts the question on the user\'s screen — has had the dispatch'],
591
+ ]);
592
+ /**
593
+ * Receivers whose members name a plugin context.
594
+ *
595
+ * The same set the Tier C detached-member check guards on, minus `process`:
596
+ * a seam is read off the context, never off `process`.
597
+ */
598
+ export const CONTEXT_RECEIVERS = new Set([
599
+ 'ctx', 'context', 'globalThis', 'global',
600
+ ]);
601
+ /**
602
+ * Array and collection methods that change the receiver rather than reading it.
603
+ *
604
+ * Used to tell a write into a service's internals from a read of them. The
605
+ * distinction is not academic: `dsh-dlp` reads
606
+ * `ctx.events._hooks['approval/request']?.length` to decide whether an ask
607
+ * would reach a human, which is an honest use of the same property a hostile
608
+ * layer splices.
609
+ */
610
+ export const MUTATING_METHODS = new Set([
611
+ 'splice', 'push', 'pop', 'shift', 'unshift', 'fill', 'sort', 'reverse', 'copyWithin',
612
+ 'clear', 'delete', 'set', 'add',
613
+ ]);
614
+ /**
615
+ * Cordis internals through which one plugin removes another plugin's
616
+ * registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
617
+ *
618
+ * None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
619
+ * `ctx.reflect` are own properties of the root context inherited by every
620
+ * child, so no `inject` declaration is needed to reach any of them.
621
+ */
622
+ export const TEARDOWN_SURFACES = [
623
+ {
624
+ service: 'events',
625
+ member: '_hooks',
626
+ readIsEnough: false,
627
+ effect: 'the listener table every layer\'s `ctx.on()` registration is stored in (`lib/index.js:230`, '
628
+ + '`_hooks = {}`, appended to by `register` at `lib/index.js:336-345`). Splicing an entry out removes that '
629
+ + 'listener permanently, and the owning layer\'s own disposer then silently does nothing. This is a stronger '
630
+ + 'reach than a waterfall veto, which only skips listeners for one dispatch',
631
+ },
632
+ {
633
+ service: 'events',
634
+ member: 'unregister',
635
+ readIsEnough: true,
636
+ effect: 'the public removal path for one listener, by callback identity (`lib/index.js:353-359`). It takes the '
637
+ + 'listener list and a callback and splices, with no check that the caller owns either',
638
+ },
639
+ {
640
+ service: 'registry',
641
+ member: 'delete',
642
+ readIsEnough: true,
643
+ effect: 'disposal of every fiber a plugin owns (`lib/index.js:1564-1571`: `for (const fiber of runtime.fibers) '
644
+ + 'fiber.dispose();`). It takes no ownership check, so one layer can unload another layer outright — '
645
+ + 'including a security layer whose guards and listeners then stop existing',
646
+ },
647
+ {
648
+ service: 'reflect',
649
+ member: 'store',
650
+ readIsEnough: false,
651
+ effect: 'the service implementation table keyed by isolate symbol (`lib/index.js:726`, written by `provide` at '
652
+ + '`lib/index.js:813`). `provide` throws when a key is already taken and `set` throws across fibers; writing '
653
+ + 'this object directly is the path around both throws',
654
+ },
655
+ ];
package/lib/npm.js CHANGED
@@ -7,16 +7,27 @@
7
7
  * fixed and their order is the guarantee:
8
8
  *
9
9
  * 1. read the version document (~3 KB) — which already answers
10
- * `hasInstallScript`, the install lifecycle scripts, and `dsh.bundle`;
10
+ * `hasInstallScript`, the install lifecycle scripts, `dsh.bundle`, and
11
+ * whether the registry holds a provenance attestation at all;
11
12
  * 2. download the tarball into memory;
12
13
  * 3. verify `dist.integrity` **before** anything parses a byte of it;
13
- * 4. decode in memory and analyse, exactly as the tarball path does.
14
+ * 4. read the provenance attestation, when step 1 said there is one, and check
15
+ * it against the bytes step 3 vouched for;
16
+ * 5. decode in memory and analyse, exactly as the tarball path does.
17
+ *
18
+ * Step 4 is the only request this module makes that is not unconditional, and
19
+ * it is skipped for every package the version document says has no attestation
20
+ * — which on the measured corpus is 28 packages in 40. It never fails an
21
+ * analysis: an endpoint that is down or a bundle that does not decode leaves
22
+ * the provenance fact in state `unreadable`, which is a different answer from
23
+ * `absent` and is printed as one.
14
24
  *
15
25
  * No subprocess, no disk write, no lifecycle script, and no `npm pack`.
16
26
  * @module dsh-plugin-inspector/npm
17
27
  */
28
+ import { provenanceAbsent, provenanceUnreadable, readProvenance, } from "./attestation.js";
18
29
  import { analyze } from "./inspect.js";
19
- import { DEFAULT_REGISTRY, fetchVerifiedTarball, parseSpec, resolvePackage, } from "./registry.js";
30
+ import { attestationUrl, DEFAULT_REGISTRY, fetchAttestation, fetchVerifiedTarball, parseSpec, RegistryError, resolvePackage, } from "./registry.js";
20
31
  import { loadTarballBuffer } from "./source.js";
21
32
  /**
22
33
  * The metadata pre-check, which needs no tarball.
@@ -31,6 +42,46 @@ import { loadTarballBuffer } from "./source.js";
31
42
  export async function precheck(spec, options = {}) {
32
43
  return resolvePackage(parseSpec(spec), options);
33
44
  }
45
+ /**
46
+ * Read the registry's provenance attestation for a resolved package, when it
47
+ * has one, and check it against the bytes that were downloaded.
48
+ *
49
+ * Every failure on this path becomes a fact rather than a refusal. The tarball
50
+ * has already been checked against `dist.integrity`, so the analysis is sound
51
+ * whatever the attestation endpoint does, and turning a registry outage into
52
+ * exit code 2 would make provenance a precondition for reading a package
53
+ * instead of something reported about it.
54
+ * @param resolved - the packument reading for the version.
55
+ * @param registry - the registry base URL, without a trailing slash.
56
+ * @param tarball - the verified tarball bytes.
57
+ * @param options - where to fetch from.
58
+ * @returns what the registry says about the build origin, and what was checked.
59
+ */
60
+ async function readRegistryProvenance(resolved, registry, tarball, options) {
61
+ if (resolved.provenancePredicateType === null)
62
+ return provenanceAbsent();
63
+ let url;
64
+ try {
65
+ url = attestationUrl(registry, resolved.name, resolved.version);
66
+ }
67
+ catch (error) {
68
+ /* v8 ignore next -- `attestationUrl` refuses a name or version only with a RegistryError. */
69
+ if (!(error instanceof RegistryError))
70
+ throw error;
71
+ return provenanceUnreadable(null, error.message);
72
+ }
73
+ let body;
74
+ try {
75
+ body = await fetchAttestation(url, options);
76
+ }
77
+ catch (error) {
78
+ /* v8 ignore next -- `fetchAttestation` reports every refusal as a RegistryError. */
79
+ if (!(error instanceof RegistryError))
80
+ throw error;
81
+ return provenanceUnreadable(url, error.message);
82
+ }
83
+ return readProvenance(body, { name: resolved.name, version: resolved.version, tarball, url });
84
+ }
34
85
  /**
35
86
  * Fetch a published package and inspect it in memory.
36
87
  * @param spec - `<name>` or `<name>@<version>`; no version means the `latest` tag.
@@ -42,9 +93,11 @@ export async function precheck(spec, options = {}) {
42
93
  export async function inspectFromNpm(spec, options = {}) {
43
94
  const resolved = await precheck(spec, options);
44
95
  const verified = await fetchVerifiedTarball(resolved, options);
96
+ const registry = (options.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, '');
97
+ const attestation = await readRegistryProvenance(resolved, registry, verified.bytes, options);
45
98
  const provenance = {
46
99
  spec,
47
- registry: (options.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, ''),
100
+ registry,
48
101
  resolvedVersion: resolved.version,
49
102
  tarball: resolved.tarball,
50
103
  digest: verified.digest,
@@ -54,5 +107,5 @@ export async function inspectFromNpm(spec, options = {}) {
54
107
  tarballBytes: verified.bytes.byteLength,
55
108
  };
56
109
  const source = await loadTarballBuffer(verified.bytes, `npm:${resolved.name}@${resolved.version}`);
57
- return analyze(source, provenance);
110
+ return analyze(source, provenance, attestation);
58
111
  }
package/lib/registry.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * @module dsh-plugin-inspector/registry
20
20
  */
21
21
  import { createHash } from 'node:crypto';
22
+ import { MAX_ATTESTATION_BYTES } from "./attestation.js";
22
23
  import { INSTALL_LIFECYCLE_SCRIPTS } from "./knowledge.js";
23
24
  import { MAX_TOTAL_BYTES } from "./source.js";
24
25
  /** The public npm registry, used when no other is named. */
@@ -146,6 +147,7 @@ export async function resolvePackage(spec, options = {}) {
146
147
  integrity: typeof dist.integrity === 'string' ? dist.integrity : null,
147
148
  shasum: typeof dist.shasum === 'string' ? dist.shasum : null,
148
149
  hasInstallScript: record.hasInstallScript === true,
150
+ provenancePredicateType: asString(asRecord(asRecord(dist.attestations).provenance), 'predicateType'),
149
151
  lifecycleScripts: INSTALL_LIFECYCLE_SCRIPTS.filter(name => typeof scripts[name] === 'string'),
150
152
  bundlePatch: typeof bundle.patch === 'string' ? bundle.patch : null,
151
153
  metadataBytes: body.byteLength,
@@ -162,6 +164,16 @@ function asRecord(value) {
162
164
  ? value
163
165
  : {};
164
166
  }
167
+ /**
168
+ * Read a string field, or `null` when it is absent or of another type.
169
+ * @param record - the containing record.
170
+ * @param key - the field name.
171
+ * @returns the string, or `null`.
172
+ */
173
+ function asString(record, key) {
174
+ const value = record[key];
175
+ return typeof value === 'string' ? value : null;
176
+ }
165
177
  /**
166
178
  * Refuse a tarball URL that points somewhere other than the registry that
167
179
  * described it.
@@ -260,3 +272,45 @@ export async function fetchVerifiedTarball(resolved, options = {}) {
260
272
  const bytes = await readCapped(response, MAX_TARBALL_BYTES, `tarball ${resolved.tarball}`);
261
273
  return verifyIntegrity(bytes, resolved);
262
274
  }
275
+ /**
276
+ * The endpoint an npm-compatible registry serves a version's attestation
277
+ * bundle from.
278
+ *
279
+ * Built from the registry base URL rather than read out of
280
+ * `dist.attestations.url`, which is the opposite of how the tarball URL is
281
+ * handled and is deliberate: the tarball has to come from wherever the registry
282
+ * says because there is no other way to name it, so that URL is taken from the
283
+ * document and then refused unless it is same-origin. An attestation needs no
284
+ * such freedom. Constructing the path here means a doctored packument cannot
285
+ * redirect the request at all, not even to another path on the same host.
286
+ *
287
+ * The name and version are re-validated because both come out of the version
288
+ * document, which is registry-controlled: `name` is not necessarily the name
289
+ * that was asked for, and it is interpolated into a URL.
290
+ * @param registry - the registry base URL, without a trailing slash.
291
+ * @param name - the resolved package name.
292
+ * @param version - the resolved version.
293
+ * @returns the absolute URL.
294
+ * @throws RegistryError when the document's name or version would not address this endpoint.
295
+ */
296
+ export function attestationUrl(registry, name, version) {
297
+ if (!PACKAGE_NAME.test(name))
298
+ throw new RegistryError(`the version document names no npm package: ${name}`);
299
+ if (!VERSION_OR_TAG.test(version))
300
+ throw new RegistryError(`the version document names no version: ${version}`);
301
+ return `${registry}/-/npm/v1/attestations/${name}@${version}`;
302
+ }
303
+ /**
304
+ * Download an attestation document.
305
+ *
306
+ * Only ever called when the version document said there is one, so a package
307
+ * without provenance costs no request at all.
308
+ * @param url - the endpoint, from {@link attestationUrl}.
309
+ * @param options - where to fetch from.
310
+ * @returns the document as served.
311
+ * @throws RegistryError on a transport failure, a non-2xx status, or an oversized body.
312
+ */
313
+ export async function fetchAttestation(url, options = {}) {
314
+ const response = await get(url, 'application/json', options);
315
+ return readCapped(response, MAX_ATTESTATION_BYTES, 'attestation document');
316
+ }