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.
@@ -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 declare const HARNESS_REFERENCE = "0.1.0-rc.5";
24
+ export declare const HARNESS_REFERENCE = "0.1.2-rc.1";
17
25
  /** The shipped bundles, each of which is one patch layer over the profile root. */
18
26
  export type BundleName = 'base' | 'headless' | 'web-app';
19
27
  /**
@@ -62,7 +70,99 @@ export declare const SECURITY_ROW_IDS: ReadonlyMap<string, string>;
62
70
  * replaces a core service for every consumer in its scope.
63
71
  */
64
72
  export declare const SEAM_KEYS: ReadonlySet<string>;
65
- /** The subset of {@link SEAM_KEYS} whose replacement removes a constraint. */
73
+ /**
74
+ * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
75
+ *
76
+ * `authorization` is the registry of flows that obtain a credential through a
77
+ * conversation with the user, so providing it means owning that conversation.
78
+ * That is the same class of substitution as `credentials`, which this set
79
+ * already holds. `fileReferences` decides which paths are offered for
80
+ * completion and `agentTeams` is the team form of `subagents`, which is
81
+ * deliberately not here either, so neither of those is in this set.
82
+ *
83
+ * Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
84
+ * services that own one `ctx.remote.*` namespace the browser client calls
85
+ * across the wire. A controller belongs here only when the traffic a
86
+ * substitution redirects to it carries a secret, an execution boundary, or a
87
+ * decision. A controller that forwards its seam's own verbs and adds a wire
88
+ * failure vocabulary does not, because the seam it fronts is reachable from
89
+ * `ctx` without substituting anything.
90
+ *
91
+ * Included:
92
+ * - `credentialsController` is what a browser configuration page calls to store
93
+ * a credential. `set(ref, value)` receives the plaintext secret and hands it
94
+ * to `ctx.credentials`
95
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
96
+ * `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
97
+ * answer to the three fields `CredentialInfo` declares. A layer that provides
98
+ * it takes both halves: every secret typed into the settings page, and the
99
+ * freedom to answer a read with the stored value.
100
+ * - `settingsController` passes `redactSecrets: true` on every remote read
101
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
102
+ * which is what keeps a `role('secret')` field out of a settings response;
103
+ * `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
104
+ * `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
105
+ * value in plaintext from the configuration page. Providing it puts the layer
106
+ * on both directions of a secret's path.
107
+ * - `sessionController` resolves each new Session's cwd from the wire request
108
+ * and hands it to `ensureSession`
109
+ * (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
110
+ * `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
111
+ * `workspace-write` root the enforcing filesystem, bash and terminal backends
112
+ * fence against. Its `prompt` verb builds the message admitted to the agent
113
+ * under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
114
+ * sandbox root for every session created from the client, and the text that
115
+ * reaches the model under the user's own source label.
116
+ * - `webhookRuntime` is what a provider adapter such as
117
+ * `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
118
+ * one built-in action creates a Session from a rule result whose fields
119
+ * include `workspacePath`, `permissionPreset` and `prompt`
120
+ * (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
121
+ * `createWebhookSession` applies that preset through
122
+ * `ctx.permissionPresets.set` before admitting the prompt
123
+ * (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
124
+ * Providing it picks the approval and sandbox preset for an agent started by
125
+ * a remote delivery with no user present.
126
+ *
127
+ * Excluded:
128
+ * - `workspaceController` forwards `request.path` to
129
+ * `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
130
+ * error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
131
+ * to `:212`). The registry it fronts is `workspaceRegistry`, which is not
132
+ * here, so the substitution reaches nothing the seam does not already offer.
133
+ * - `directoryPickerController` is three delegations to
134
+ * `ctx.directoryPicker.capability()` behind a check that refuses a verb the
135
+ * composed backend does not serve
136
+ * (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
137
+ * Path fencing lives in the backend, and `directoryPicker` is not here.
138
+ * - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
139
+ * is excluded above for the same reason: the traffic is path candidates
140
+ * offered for completion.
141
+ * - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
142
+ * for one Session's human-facing composer
143
+ * (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
144
+ * `:227`), and its only consumer in this release is the client skill picker
145
+ * (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
146
+ * model through `ctx.skills.list()` in
147
+ * `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
148
+ * - `subagentModelSelection` is a settings owner answering `{ enabled,
149
+ * allowedModels }`, sampled when an Agent receives its delegation tools. Model
150
+ * routing is `llm` and delegation is `subagents`; neither is here.
151
+ * - `deepseekLlmApiExtensions` hands a substitute the serialized request body
152
+ * and merges the fields it returns, but
153
+ * `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
154
+ * field colliding with the base request, so `messages`, `tools` and `model`
155
+ * are not writable through it. The constraint is in the adapter, not the
156
+ * registry the substitution replaces.
157
+ * - `inspector` is declared by the catalogue and implemented by no shipped
158
+ * package: in `0.1.2-rc.1` the key, `InspectorJsonValue` and
159
+ * `CordisRuntimeTreeReader` occur only in
160
+ * `@deepseek-ai/dsh-tool-cordis/lib/index.js`, and nothing provides or reads
161
+ * `ctx.inspector`. No composed row enforces anything through it, so a
162
+ * substitution displaces nothing. This is the one entry decided from the
163
+ * catalogue rather than from an implementation; a release that ships one is a
164
+ * reason to decide it again.
165
+ */
66
166
  export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
67
167
  /**
68
168
  * Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
@@ -70,9 +170,24 @@ export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
70
170
  * without calling it short-circuits the chain including the built-in behavior.
71
171
  *
72
172
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
173
+ *
174
+ * `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
175
+ * and adds `user-questions/request`. Both replace content in a durable log copy
176
+ * or answer a pending request; neither is an `emit` event, so both hand a
177
+ * listener the trailing `next`.
73
178
  */
74
179
  export declare const WATERFALL_EVENTS: ReadonlySet<string>;
75
- /** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
180
+ /**
181
+ * Waterfall events whose short-circuit removes a decision the user would
182
+ * otherwise make.
183
+ *
184
+ * `user-questions/request` is here for the same reason `approval/request` is:
185
+ * `ctx.userQuestions` pauses a tool call until a human answers, and the
186
+ * answerers that put the question on a screen are listeners in the chain rather
187
+ * than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
188
+ * A listener that returns an answer without calling `next()` answers on the
189
+ * user's behalf and the question is never shown.
190
+ */
76
191
  export declare const DECISION_EVENTS: ReadonlySet<string>;
77
192
  /**
78
193
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -188,4 +303,97 @@ export declare const HARNESS_INERT_CALLS: ReadonlySet<string>;
188
303
  * in code, and it is plain YAML.
189
304
  */
190
305
  export declare const SERVICE_REMAPPING_FIELDS: readonly string[];
306
+ /**
307
+ * How a Cordis waterfall listener delegates, and what happens when it does not.
308
+ *
309
+ * Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
310
+ * `lib/index.js:317-327`:
311
+ *
312
+ * ```js
313
+ * waterfall(...args) {
314
+ * const cbs = this.dispatch("waterfall", args);
315
+ * const inner = args.pop();
316
+ * const next = () => { return (cbs.shift() ?? inner)(...args); };
317
+ * args.push(next);
318
+ * return next();
319
+ * }
320
+ * ```
321
+ *
322
+ * `next` is the trailing argument every listener receives, and `inner` is the
323
+ * harness's own built-in behavior. A listener that returns without calling
324
+ * `next()` therefore ends the chain: neither the listeners still in `cbs` nor
325
+ * `inner` run.
326
+ *
327
+ * The scope of that is one dispatch, not the registry. `dispatch()` builds
328
+ * `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
329
+ * never touched and every skipped listener is registered and runs normally on
330
+ * the next dispatch. The precise word is veto, not removal — Cordis's own
331
+ * JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
332
+ * the built-in behavior". Removal is a different capability with a different
333
+ * reach, and it has its own table below.
334
+ */
335
+ export declare const WATERFALL_NEXT_PARAMETER = "next";
336
+ /**
337
+ * What each decision waterfall's built-in `next` settles on when no listener
338
+ * claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
339
+ *
340
+ * This is what a listener that never calls `next()` replaces. The inner
341
+ * callback is the last argument at each site:
342
+ * - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
343
+ * `() => Promise.resolve({ kind: "allow" })`
344
+ * - `tools/execute` — `dsh-tools/lib/index.js:3214`,
345
+ * `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
346
+ * body of the tool call itself
347
+ * - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
348
+ * `() => Promise.resolve("unavailable")`, and the surface that would ask the
349
+ * user is one of the listeners in the chain rather than the inner callback
350
+ * - `user-questions/request` —
351
+ * `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
352
+ * callback passed at `:69`, which rejects with a `UserQuestionError` carrying
353
+ * code `NO_PROVIDER`
354
+ *
355
+ * The three tables in this module that name events (`WATERFALL_EVENTS`,
356
+ * `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
357
+ */
358
+ export declare const DECISION_EVENT_DEFAULTS: ReadonlyMap<string, string>;
359
+ /**
360
+ * Receivers whose members name a plugin context.
361
+ *
362
+ * The same set the Tier C detached-member check guards on, minus `process`:
363
+ * a seam is read off the context, never off `process`.
364
+ */
365
+ export declare const CONTEXT_RECEIVERS: ReadonlySet<string>;
366
+ /**
367
+ * Array and collection methods that change the receiver rather than reading it.
368
+ *
369
+ * Used to tell a write into a service's internals from a read of them. The
370
+ * distinction is not academic: `dsh-dlp` reads
371
+ * `ctx.events._hooks['approval/request']?.length` to decide whether an ask
372
+ * would reach a human, which is an honest use of the same property a hostile
373
+ * layer splices.
374
+ */
375
+ export declare const MUTATING_METHODS: ReadonlySet<string>;
376
+ /** One Cordis bookkeeping surface that owns other plugins' registrations. */
377
+ export interface TeardownSurface {
378
+ /** The member read off the context, e.g. `events`. */
379
+ readonly service: string;
380
+ /** The member read off that, e.g. `_hooks`. */
381
+ readonly member: string;
382
+ /**
383
+ * True when merely naming the surface is the finding. False when only a
384
+ * write counts, because reading it is something an honest plugin does.
385
+ */
386
+ readonly readIsEnough: boolean;
387
+ /** What reaching it does, phrased for a report. */
388
+ readonly effect: string;
389
+ }
390
+ /**
391
+ * Cordis internals through which one plugin removes another plugin's
392
+ * registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
393
+ *
394
+ * None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
395
+ * `ctx.reflect` are own properties of the root context inherited by every
396
+ * child, so no `inject` declaration is needed to reach any of them.
397
+ */
398
+ export declare const TEARDOWN_SURFACES: readonly TeardownSurface[];
191
399
  //# sourceMappingURL=knowledge.d.ts.map
@@ -9,6 +9,7 @@
9
9
  * would fire on every legitimate plugin and train users to ignore the tool.
10
10
  * @module dsh-plugin-inspector/model
11
11
  */
12
+ import type { ProvenanceFact } from './attestation.ts';
12
13
  /** How much a finding should weigh on an install decision. */
13
14
  export type Severity = 'critical' | 'high' | 'medium' | 'low';
14
15
  /**
@@ -25,7 +26,11 @@ export declare const SEVERITY_RANK: Readonly<Record<Severity, number>>;
25
26
  export declare const SEVERITIES: readonly Severity[];
26
27
  /** Where in the analysed package a finding was observed. */
27
28
  export interface Evidence {
28
- /** Package-relative path of the file the finding came from. */
29
+ /**
30
+ * Package-relative path of the file the finding came from, or the name of the
31
+ * registry field it came from when the finding is about what the registry
32
+ * published rather than about the package contents.
33
+ */
29
34
  readonly file: string;
30
35
  /** A locator inside that file: a YAML path, a JSON pointer, or `line:column`. */
31
36
  readonly path?: string;
@@ -93,6 +98,13 @@ export interface Facts {
93
98
  readonly packageName: string;
94
99
  readonly packageVersion: string;
95
100
  readonly license: string | null;
101
+ /**
102
+ * What the registry says about where this tarball was built, and which of
103
+ * those claims this run checked. Always present: the two modes that read
104
+ * local bytes report `unavailable`, which is a different answer from a
105
+ * published package that has no attestation.
106
+ */
107
+ readonly provenance: ProvenanceFact;
96
108
  /** True when `package.json` declares `dsh.bundle.patch` — a mounted patch layer. */
97
109
  readonly mountsAsBundle: boolean;
98
110
  /** The declared patch path, verbatim and unresolved, or `null`. */
@@ -7,10 +7,20 @@
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
@@ -54,6 +54,13 @@ export interface ResolvedPackage {
54
54
  readonly lifecycleScripts: readonly string[];
55
55
  /** The `dsh.bundle.patch` value, which is what makes a package a mounted layer. */
56
56
  readonly bundlePatch: string | null;
57
+ /**
58
+ * The predicate type of the provenance attestation the registry says it
59
+ * holds for this version, from `dist.attestations.provenance`, or `null` when
60
+ * it says it holds none. Reading it here is what keeps the attestation
61
+ * endpoint unasked for the majority of packages that have no attestation.
62
+ */
63
+ readonly provenancePredicateType: string | null;
57
64
  /** Bytes of metadata read to learn all of the above. */
58
65
  readonly metadataBytes: number;
59
66
  }
@@ -116,4 +123,37 @@ export declare function verifyIntegrity(bytes: Buffer, resolved: ResolvedPackage
116
123
  * @throws RegistryError on a transport failure, an oversized body, or a hash mismatch.
117
124
  */
118
125
  export declare function fetchVerifiedTarball(resolved: ResolvedPackage, options?: RegistryOptions): Promise<VerifiedTarball>;
126
+ /**
127
+ * The endpoint an npm-compatible registry serves a version's attestation
128
+ * bundle from.
129
+ *
130
+ * Built from the registry base URL rather than read out of
131
+ * `dist.attestations.url`, which is the opposite of how the tarball URL is
132
+ * handled and is deliberate: the tarball has to come from wherever the registry
133
+ * says because there is no other way to name it, so that URL is taken from the
134
+ * document and then refused unless it is same-origin. An attestation needs no
135
+ * such freedom. Constructing the path here means a doctored packument cannot
136
+ * redirect the request at all, not even to another path on the same host.
137
+ *
138
+ * The name and version are re-validated because both come out of the version
139
+ * document, which is registry-controlled: `name` is not necessarily the name
140
+ * that was asked for, and it is interpolated into a URL.
141
+ * @param registry - the registry base URL, without a trailing slash.
142
+ * @param name - the resolved package name.
143
+ * @param version - the resolved version.
144
+ * @returns the absolute URL.
145
+ * @throws RegistryError when the document's name or version would not address this endpoint.
146
+ */
147
+ export declare function attestationUrl(registry: string, name: string, version: string): string;
148
+ /**
149
+ * Download an attestation document.
150
+ *
151
+ * Only ever called when the version document said there is one, so a package
152
+ * without provenance costs no request at all.
153
+ * @param url - the endpoint, from {@link attestationUrl}.
154
+ * @param options - where to fetch from.
155
+ * @returns the document as served.
156
+ * @throws RegistryError on a transport failure, a non-2xx status, or an oversized body.
157
+ */
158
+ export declare function fetchAttestation(url: string, options?: RegistryOptions): Promise<Buffer>;
119
159
  //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Readings of a syntax tree that both capability detection and readability
3
+ * detection need, and that must agree between them.
4
+ *
5
+ * Tier B matches a name; Tier C reports the names it could not match. If the
6
+ * two disagree about which expressions are constant, a package gets both a
7
+ * finding and a degrade for the same site, or neither. They agree because they
8
+ * ask the same function.
9
+ *
10
+ * Nothing here evaluates anything. {@link foldConstantString} reads literals
11
+ * out of an already-parsed tree and concatenates them; it never constructs a
12
+ * function, and it never touches an identifier's value.
13
+ * @module dsh-plugin-inspector/syntax
14
+ */
15
+ import ts from 'typescript';
16
+ /**
17
+ * The text a constant string expression holds, or `null` when the expression is
18
+ * not constant.
19
+ *
20
+ * Four forms, chosen because each one is a spelling of a name that a reader
21
+ * sees and a name-matching check does not: a literal, a `+` chain of them, a
22
+ * template whose every span is one, and `[…].join(…)` over an array of them.
23
+ * Anything reaching an identifier, a property, or any other call answers
24
+ * `null` — resolving those is value tracking, which this tool does not do and
25
+ * which Tier C exists to admit.
26
+ * @param node - the expression, or `undefined` for a missing argument.
27
+ * @returns the text, or `null`.
28
+ */
29
+ export declare function foldConstantString(node: ts.Node | undefined): string | null;
30
+ /**
31
+ * The Node API that hands back a builtin module without `require` and without
32
+ * an `import` declaration, added in Node 22.3.
33
+ *
34
+ * It reaches the same modules the harness sandbox's `require` trap covers,
35
+ * from a call that sandbox never sees: the sandbox leaves
36
+ * `process` `undefined`, so inside it this expression throws, and a mounted
37
+ * bundle layer is not inside it.
38
+ * @see https://nodejs.org/api/process.html#processgetbuiltinmoduleid
39
+ */
40
+ export declare const BUILTIN_MODULE_GETTER = "getBuiltinModule";
41
+ /**
42
+ * Whether a call is `process.getBuiltinModule(…)`.
43
+ *
44
+ * The receiver is required. `getBuiltinModule` pulled off `process` and bound
45
+ * to a bare name is not this — it is a detached member, which Tier C reports as
46
+ * dispatch it cannot follow.
47
+ * @param node - the call expression.
48
+ * @returns true when the call loads a builtin through `process`.
49
+ */
50
+ export declare function isBuiltinModuleGetter(node: ts.CallExpression): boolean;
51
+ //# sourceMappingURL=syntax.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-inspector",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Know what a DeepSeek Harness plugin does before you install it — static pre-install analysis of a plugin directory or tarball",
5
5
  "license": "MIT",
6
6
  "author": "Ivan Tyshchenko",
@@ -57,6 +57,7 @@
57
57
  "test:coverage": "vitest run --config vitest.config.ts --coverage",
58
58
  "inspect": "node --experimental-strip-types src/cli.ts",
59
59
  "sweep": "node --experimental-strip-types scripts/ecosystem-sweep.ts",
60
+ "sync": "node --experimental-strip-types scripts/harness-sync.ts",
60
61
  "test:e2e": "pnpm run build && vitest run --config vitest.e2e.config.ts"
61
62
  }
62
63
  }