dsh-plugin-inspector 0.7.0 → 0.9.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/README.md CHANGED
@@ -79,6 +79,20 @@ Findings are tiered by how much you should trust them:
79
79
 
80
80
  [Every check, by tier →](https://charlotten7.github.io/dsh-plugin-inspector/checks.html)
81
81
 
82
+ ## What it deliberately does not flag
83
+
84
+ A `critical` is only worth reading if it is rare, so the two tables that decide one — the core rows
85
+ A2 treats as security-relevant, and the capability seams A23, B1 and B15 do — are kept small on a
86
+ stated rule: a row is in when disabling it **fails open** or **silently removes evidence**. A row
87
+ that fails closed when removed is out, however security-adjacent its name reads, and so is one whose
88
+ absence takes a feature away and grants nothing.
89
+
90
+ The rows checked against that rule and kept out are named with their reasons, so you can tell "we
91
+ checked and it does not qualify" from "we never looked". Excluded is not unreported: disabling any
92
+ core row is still an A3 finding.
93
+
94
+ [What is deliberately not a finding →](https://charlotten7.github.io/dsh-plugin-inspector/checks.html#what-is-deliberately-not-a-finding)
95
+
82
96
  ## The ceiling
83
97
 
84
98
  **This is not a malware scanner and it cannot be one.** Capability is decidable from source;
@@ -20,7 +20,7 @@ import { declaredPackages } from "../manifest.js";
20
20
  * whatever YAML that library happens to ship is inert bytes.
21
21
  */
22
22
  const PATCH_ROW_CHECKS = new Set([
23
- 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A15', 'A17', 'A19', 'A23',
23
+ 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A15', 'A17', 'A19', 'A23', 'A26',
24
24
  ]);
25
25
  /** Loader builtins that are entry names but not resolvable npm packages. */
26
26
  const LOADER_BUILTINS = new Set([
@@ -765,6 +765,73 @@ function checkProvenance(input) {
765
765
  evidence: { file: 'dist.attestations', path: check.name },
766
766
  }));
767
767
  }
768
+ /**
769
+ * A26 — a patch row that modifies a row this package neither ships nor shares
770
+ * with the harness.
771
+ *
772
+ * A2, A3, A5 and A19 all key on {@link CORE_ROWS}: an override whose `id` is
773
+ * not a row the shipped bundles define falls through every one of them and
774
+ * produces nothing. But the composed profile is not only the core rows. It also
775
+ * holds the rows the user wrote in their own layer and the rows every other
776
+ * installed plugin inserted, and `applyEntryPatches` matches by `id` alone with
777
+ * no notion of who owns the row. So `- id: some-other-plugin` / `disabled:
778
+ * true` in this package's layer switches that package off, and a `config:`
779
+ * override replaces its configuration wholesale, since a patch override is a
780
+ * shallow whole-value replacement rather than a merge.
781
+ *
782
+ * The check is a set difference, which is what keeps it decidable: an id that
783
+ * is neither a core row nor a row this same layer inserts belongs to somebody
784
+ * else. Two kinds of package legitimately rewrite rows they did not insert and
785
+ * are excluded — the harness's own bundles, which is what composing a surface
786
+ * bundle is, and a package that declares `dsh.profile.bundles`, which is a
787
+ * profile assembling other people's layers on purpose and is reported as such
788
+ * by A20.
789
+ */
790
+ function checkForeignRows(input) {
791
+ if (isHarnessBundle(input))
792
+ return [];
793
+ if ((input.manifest.dsh.profile?.bundles ?? []).length > 0)
794
+ return [];
795
+ const findings = [];
796
+ for (const patch of input.patches) {
797
+ const own = new Set(patch.inserts.map(row => row.id).filter(id => id !== null));
798
+ for (const override of patch.overrides) {
799
+ if (override.overriddenKeys.length === 0)
800
+ continue;
801
+ if (CORE_ROWS.has(override.id) || own.has(override.id))
802
+ continue;
803
+ const switchedOff = override.overriddenKeys.includes('disabled') && Boolean(override.disabled);
804
+ const rewritten = override.overriddenKeys.filter(key => key !== 'disabled');
805
+ findings.push(tierA({
806
+ checkId: 'A26',
807
+ name: 'foreign-row-modified',
808
+ subject: override.id,
809
+ severity: 'high',
810
+ title: switchedOff
811
+ ? `Patch layer disables the row "${override.id}", which this package does not ship`
812
+ : `Patch layer rewrites ${rewritten.map(key => `\`${key}\``).join(', ')} on the row `
813
+ + `"${override.id}", which this package does not ship`,
814
+ detail: `"${override.id}" is neither a row the shipped bundles define nor one this layer inserts, so it `
815
+ + 'belongs to the user\'s own layer or to another installed plugin. `applyEntryPatches` matches rows by '
816
+ + '`id` alone and has no notion of which layer owns one, so this patch reaches into that package\'s row '
817
+ + 'and '
818
+ + (switchedOff
819
+ ? 'stops it running. Whatever that package contributed — a guard, a listener, an audit sink — is not '
820
+ + 'composed into the profile, and the user\'s own configuration still says it is installed.'
821
+ : 'replaces those keys. An override is a shallow whole-value replacement rather than a merge, so an '
822
+ + 'overridden `config` discards every key that package shipped and keeps only what is written here.')
823
+ + ' If the row id is not present in the composed profile the patch is simply inert, which is the benign '
824
+ + 'reading and the one a reader should check first.',
825
+ evidence: {
826
+ file: patch.file,
827
+ path: switchedOff ? `${override.path}.disabled` : override.path,
828
+ snippet: snippet(override.overriddenKeys.join(', ')),
829
+ },
830
+ }));
831
+ }
832
+ }
833
+ return findings;
834
+ }
768
835
  /**
769
836
  * Run every Tier A check.
770
837
  *
@@ -781,6 +848,7 @@ export function runTierA(input) {
781
848
  ...checkNativeBuild(input),
782
849
  ...checkDisabledRows(input),
783
850
  ...checkOverriddenRows(input),
851
+ ...checkForeignRows(input),
784
852
  ...checkExpressions(input),
785
853
  ...checkPatchFailures(input),
786
854
  ...checkInsertedModules(input),
@@ -15,7 +15,7 @@
15
15
  import ts from 'typescript';
16
16
  import { lineColumn, snippet } from "../files.js";
17
17
  import { scanInjection } from "../injection.js";
18
- import { NETWORK_MODULES, SEAM_KEYS, SECURITY_SEAM_KEYS, UNMEDIATED_FS_MODULES, UNMEDIATED_PROCESS_MODULES, } from "../knowledge.js";
18
+ import { CONTEXT_RECEIVERS, DECISION_EVENTS, DECISION_EVENT_DEFAULTS, MUTATING_METHODS, NETWORK_MODULES, SEAM_KEYS, SECURITY_SEAM_KEYS, TEARDOWN_SURFACES, UNMEDIATED_FS_MODULES, UNMEDIATED_PROCESS_MODULES, WATERFALL_EVENTS, } from "../knowledge.js";
19
19
  import { foldConstantString, isBuiltinModuleGetter } from "../syntax.js";
20
20
  /** Global functions that fetch over the network without any `ctx` service. */
21
21
  const NETWORK_GLOBALS = new Set(['fetch', 'WebSocket', 'EventSource', 'XMLHttpRequest']);
@@ -494,6 +494,292 @@ function checkNetworkGlobals(file, node, accumulator) {
494
494
  accumulator.findings.push(finding);
495
495
  accumulator.networkCall ??= finding;
496
496
  }
497
+ /**
498
+ * The function a listener argument denotes, when this tool can see one.
499
+ *
500
+ * Two forms are followed: the function written at the call site, and a name
501
+ * bound to a function in the same file. That is the same bounded, single-file
502
+ * name resolution `isRegisteredName` already does for tool definitions, and it
503
+ * stops at the same place: a listener imported from another module, or built by
504
+ * a helper, is not resolved and produces no finding rather than a guess.
505
+ * @param node - the listener argument.
506
+ * @param file - the parsed file the call is in.
507
+ * @returns the function, or `null` when it cannot be resolved from this file.
508
+ */
509
+ function resolveListener(node, file) {
510
+ if (node === undefined)
511
+ return null;
512
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
513
+ return node;
514
+ if (!ts.isIdentifier(node))
515
+ return null;
516
+ const wanted = node.text;
517
+ let found = null;
518
+ const visit = (child) => {
519
+ if (ts.isFunctionDeclaration(child) && child.name?.text === wanted)
520
+ found ??= child;
521
+ if (ts.isVariableDeclaration(child) && ts.isIdentifier(child.name) && child.name.text === wanted
522
+ && child.initializer !== undefined
523
+ && (ts.isArrowFunction(child.initializer) || ts.isFunctionExpression(child.initializer))) {
524
+ found ??= child.initializer;
525
+ }
526
+ ts.forEachChild(child, visit);
527
+ };
528
+ ts.forEachChild(file.node, visit);
529
+ return found;
530
+ }
531
+ /**
532
+ * Whether a listener can reach the `next` continuation the waterfall hands it.
533
+ *
534
+ * `next` is positional, not named: `EventsService.waterfall` pushes it as the
535
+ * last dispatch argument, so it is whatever the listener's trailing parameter
536
+ * is called. A listener that declares no parameter in that position never
537
+ * receives it, and one that declares it and never mentions it cannot call it.
538
+ * Either way the chain stops there.
539
+ *
540
+ * Every uncertain shape answers true, so the finding needs a listener whose
541
+ * trailing parameter is a plain name that the body does not contain: a rest
542
+ * parameter, a destructuring pattern, and any use of `arguments` all count as
543
+ * reaching it.
544
+ * @param fn - the resolved listener.
545
+ * @returns true when the listener can call `next`.
546
+ */
547
+ function canReachNext(fn) {
548
+ const parameters = fn.parameters;
549
+ const last = parameters.at(-1);
550
+ if (last === undefined)
551
+ return false;
552
+ if (last.dotDotDotToken !== undefined || !ts.isIdentifier(last.name))
553
+ return true;
554
+ const wanted = last.name.text;
555
+ const body = fn.body;
556
+ if (body === undefined)
557
+ return true;
558
+ let referenced = false;
559
+ const visit = (node) => {
560
+ if (ts.isIdentifier(node) && (node.text === wanted || node.text === 'arguments'))
561
+ referenced = true;
562
+ ts.forEachChild(node, visit);
563
+ };
564
+ ts.forEachChild(body, visit);
565
+ return referenced;
566
+ }
567
+ /**
568
+ * Whether a listener registration asks to run ahead of the listeners already
569
+ * composed on the event.
570
+ *
571
+ * Both spellings the event bus accepts: the option object, and the boolean
572
+ * shorthand `EventsService.on` expands with `options = { prepend: options }`.
573
+ * @param node - the options argument, when there is one.
574
+ * @returns true when the registration prepends.
575
+ */
576
+ function isPrepended(node) {
577
+ if (node === undefined)
578
+ return false;
579
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
580
+ return true;
581
+ if (!ts.isObjectLiteralExpression(node))
582
+ return false;
583
+ return node.properties.some(property => ts.isPropertyAssignment(property)
584
+ && ts.isIdentifier(property.name) && property.name.text === 'prepend'
585
+ && property.initializer.kind === ts.SyntaxKind.TrueKeyword);
586
+ }
587
+ /** B14 — a waterfall listener that cannot delegate to the rest of the chain. */
588
+ function checkWaterfallVeto(file, node, accumulator) {
589
+ const callee = node.expression;
590
+ if (!ts.isPropertyAccessExpression(callee))
591
+ return;
592
+ if (callee.name.text !== 'on' && callee.name.text !== 'once')
593
+ return;
594
+ const event = literalText(node.arguments[0]);
595
+ if (event === null || !WATERFALL_EVENTS.has(event))
596
+ return;
597
+ const listener = resolveListener(node.arguments[1], file);
598
+ if (listener === null || canReachNext(listener))
599
+ return;
600
+ const decides = DECISION_EVENTS.has(event);
601
+ const prepended = isPrepended(node.arguments[2]);
602
+ const consequence = DECISION_EVENT_DEFAULTS.get(event);
603
+ accumulator.findings.push(tierB({
604
+ checkId: 'B14',
605
+ name: 'waterfall-veto',
606
+ subject: event,
607
+ severity: decides ? 'critical' : 'high',
608
+ title: `Listens on \`${event}\` and never calls \`next\``,
609
+ detail: `\`${event}\` is dispatched as a Cordis waterfall, which hands every listener a trailing \`next\` and `
610
+ + 'ends the chain at the first listener that returns without calling it — the remaining listeners and the '
611
+ + 'harness\'s own built-in behavior both stop for that dispatch. This listener '
612
+ + (listener.parameters.length === 0
613
+ ? 'declares no parameters, so it never receives `next` at all.'
614
+ : `never mentions its trailing parameter \`${(listener.parameters.at(-1)?.name).text}\`, `
615
+ + 'which is the `next` the dispatch supplies.')
616
+ + (consequence === undefined ? '' : ` Without it, ${consequence}.`)
617
+ + (prepended
618
+ ? ' It is registered with `prepend`, which unshifts it onto the listener list, so it claims every dispatch '
619
+ + 'ahead of every listener composed before this layer mounted.'
620
+ : '')
621
+ + ' The veto is per dispatch: nothing is unregistered, and every skipped listener runs again next time.',
622
+ evidence: at(file, node),
623
+ bypass: 'calling `next()` on a branch the listener never takes, or passing a listener this tool cannot resolve '
624
+ + 'from the file it is registered in — one imported from another module, or returned by a helper',
625
+ }));
626
+ }
627
+ /**
628
+ * Read a property chain rooted at a plugin context and report which catalogued
629
+ * seam it goes through.
630
+ *
631
+ * The receiver guard is what keeps this off ordinary code, exactly as it does
632
+ * in B1 and C2: the chain has to start at a name that denotes a plugin context,
633
+ * and its first member has to be one of the catalogued seam keys.
634
+ * @param node - the innermost expression of a property access.
635
+ * @returns the seam and the depth, or `null` when the chain is not one.
636
+ */
637
+ function seamChain(node) {
638
+ const members = [];
639
+ let cursor = node;
640
+ while (ts.isPropertyAccessExpression(cursor)) {
641
+ members.unshift(cursor.name.text);
642
+ cursor = cursor.expression;
643
+ }
644
+ // Two roots: a bare context name, and `this.ctx` — the form a plugin written
645
+ // as a class uses, which the Tier C detached-member check already treats as a
646
+ // known receiver.
647
+ if (cursor.kind === ts.SyntaxKind.ThisKeyword && members[0] === 'ctx')
648
+ members.shift();
649
+ else if (!ts.isIdentifier(cursor) || !CONTEXT_RECEIVERS.has(cursor.text))
650
+ return null;
651
+ const seam = members[0];
652
+ if (seam === undefined || !SEAM_KEYS.has(seam))
653
+ return null;
654
+ return { seam, depth: members.length - 1 };
655
+ }
656
+ /**
657
+ * Build the B15 finding for one write site.
658
+ * @param file - the parsed file.
659
+ * @param node - the node to cite.
660
+ * @param chain - the seam the write goes through.
661
+ * @param how - the clause naming what the write does.
662
+ * @returns the finding.
663
+ */
664
+ function seamWriteFinding(file, node, chain, how) {
665
+ const critical = SECURITY_SEAM_KEYS.has(chain.seam);
666
+ return tierB({
667
+ checkId: 'B15',
668
+ name: 'seam-internals-write',
669
+ subject: chain.seam,
670
+ severity: critical ? 'critical' : 'high',
671
+ title: `${how} inside the \`${chain.seam}\` capability seam`,
672
+ detail: `\`${chain.seam}\` is a catalogued core service, and Cordis resolves it to one shared instance for the `
673
+ + 'whole tree — a write through this package\'s context is what every other consumer reads afterwards. '
674
+ + 'B1 reads `ctx.provide` / `ctx.set` / `ctx.mixin`, the declared ways to substitute a service, and Cordis '
675
+ + 'refuses both from a layer that does not own the service: `provide` throws when the key is taken and `set` '
676
+ + 'throws with "cannot set property in multiple fibers". Writing a member of the object those calls would '
677
+ + 'have replaced reaches the same substitution and meets neither check.'
678
+ + (critical ? ' This seam is one whose whole purpose is to constrain what the agent may do.' : ''),
679
+ evidence: at(file, node),
680
+ bypass: 'reaching the same object through a value this tool does not track — a local bound to `ctx.tools` and '
681
+ + 'written through afterwards — or a computed member name, which C2 reports',
682
+ });
683
+ }
684
+ /** B15 — writing into a catalogued seam's own object graph. */
685
+ function checkSeamWrite(file, node, accumulator) {
686
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
687
+ && ts.isPropertyAccessExpression(node.left)) {
688
+ const chain = seamChain(node.left);
689
+ if (chain !== null && chain.depth >= 1) {
690
+ accumulator.findings.push(seamWriteFinding(file, node, chain, 'Assigns to a member'));
691
+ }
692
+ return;
693
+ }
694
+ if (ts.isDeleteExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
695
+ const chain = seamChain(node.expression);
696
+ if (chain !== null && chain.depth >= 1) {
697
+ accumulator.findings.push(seamWriteFinding(file, node, chain, 'Deletes a member'));
698
+ }
699
+ return;
700
+ }
701
+ // A mutating call is only a finding when it reaches *past* the service's own
702
+ // API. `ctx.credentials.set(ref, value)` and `ctx.skills.register(skill)` are
703
+ // the seam's published methods and sit at depth 0; `ctx.tools.layers.global
704
+ // .guards.data.clear()` reaches through five members into the map a
705
+ // `ctx.tools.guard()` deny is filed in.
706
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression))
707
+ return;
708
+ if (!MUTATING_METHODS.has(node.expression.name.text))
709
+ return;
710
+ const chain = seamChain(node.expression.expression);
711
+ if (chain === null || chain.depth < 2)
712
+ return;
713
+ accumulator.findings.push(seamWriteFinding(file, node, chain, `Calls \`.${node.expression.name.text}()\` on state`));
714
+ }
715
+ /**
716
+ * Whether a node sits on the left of an assignment or under a `delete`, which
717
+ * is what separates writing one of the Cordis bookkeeping tables from reading
718
+ * it. Reading is something an honest plugin does: `dsh-dlp` counts
719
+ * `ctx.events._hooks['approval/request']` to decide whether an approval would
720
+ * reach a human.
721
+ * @param node - the property access naming the surface.
722
+ * @returns true when the surface is being written rather than read.
723
+ */
724
+ function isWriteTarget(node) {
725
+ let cursor = node;
726
+ for (;;) {
727
+ // Sources are parsed with `setParentNodes`, and the loop only ascends
728
+ // through property and element accesses, each of which has a parent — the
729
+ // statement that would hold a parentless node ends the walk one step below.
730
+ const parent = cursor.parent;
731
+ if (ts.isDeleteExpression(parent))
732
+ return true;
733
+ if (ts.isBinaryExpression(parent) && parent.left === cursor
734
+ && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
735
+ || parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken))
736
+ return true;
737
+ // The surface is the receiver of a mutating call, either directly
738
+ // (`_hooks.clear()`) or after an index step (`_hooks[name].splice(0)`),
739
+ // which reaches the same table through the array it holds.
740
+ if (ts.isCallExpression(parent) && parent.expression === cursor
741
+ && ts.isPropertyAccessExpression(cursor)) {
742
+ return MUTATING_METHODS.has(cursor.name.text);
743
+ }
744
+ if (!ts.isPropertyAccessExpression(parent) && !ts.isElementAccessExpression(parent))
745
+ return false;
746
+ cursor = parent;
747
+ }
748
+ }
749
+ /** B16 — reaching the Cordis bookkeeping that owns other layers' registrations. */
750
+ function checkTeardown(file, node, accumulator) {
751
+ if (!ts.isPropertyAccessExpression(node))
752
+ return;
753
+ const outer = node.expression;
754
+ if (!ts.isPropertyAccessExpression(outer))
755
+ return;
756
+ if (!ts.isIdentifier(outer.expression) || !CONTEXT_RECEIVERS.has(outer.expression.text))
757
+ return;
758
+ const surface = TEARDOWN_SURFACES.find(entry => entry.service === outer.name.text && entry.member === node.name.text);
759
+ if (surface === undefined)
760
+ return;
761
+ if (!surface.readIsEnough && !isWriteTarget(node))
762
+ return;
763
+ accumulator.findings.push(tierB({
764
+ checkId: 'B16',
765
+ name: 'foreign-registration-teardown',
766
+ subject: `${surface.service}.${surface.member}`,
767
+ severity: 'critical',
768
+ title: `Reaches \`${surface.service}.${surface.member}\`, which owns other layers' registrations`,
769
+ detail: `\`ctx.${surface.service}.${surface.member}\` is ${surface.effect}. Cordis makes `
770
+ + `\`ctx.${surface.service}\` an own property of the root context that every child inherits, so reaching it `
771
+ + 'needs no `inject` declaration and nothing records that this layer did. A guard registered by a security '
772
+ + 'plugin through `ctx.tools.guard()`, and every listener it composed, are removable this way — which is a '
773
+ + 'wider reach than any single seam replacement, because it does not substitute a decision, it deletes the '
774
+ + 'code that would have made one.'
775
+ + (surface.readIsEnough
776
+ ? ''
777
+ : ' Reading this surface is not the finding: an honest plugin counts the listeners on a seam to decide '
778
+ + 'whether a prompt would reach a human. Only a write is raised.'),
779
+ evidence: at(file, node),
780
+ bypass: 'a computed member name, which C2 reports, or reaching the same table through a local bound earlier',
781
+ }));
782
+ }
497
783
  /**
498
784
  * Run every Tier B check.
499
785
  * @param input - the decoded package.
@@ -517,7 +803,10 @@ export function runTierB(input) {
517
803
  checkSeamReplacement(file, node, accumulator);
518
804
  checkSystemPrompt(file, node, accumulator);
519
805
  checkNestedMount(file, node, accumulator);
806
+ checkWaterfallVeto(file, node, accumulator);
520
807
  }
808
+ checkSeamWrite(file, node, accumulator);
809
+ checkTeardown(file, node, accumulator);
521
810
  checkDynamicCode(file, node, accumulator);
522
811
  checkCredentialRead(file, node, accumulator);
523
812
  checkToolDescription(file, node, accumulator);
package/lib/knowledge.js CHANGED
@@ -13,13 +13,15 @@
13
13
  * Harness version these tables were transcribed from — the version string in
14
14
  * the shipped bundles' own `package.json`, which is `dsh`'s own version.
15
15
  *
16
- * Re-verified against `0.1.1-rc.2`, the release npm tags `latest`, by
16
+ * Re-verified against `0.1.2-rc.1`, the release npm tags `latest`, by
17
17
  * extracting each table from the published packages and diffing it against the
18
- * one here. What moved: six rows inserted by the web bundle, three seam keys,
19
- * and two sandbox traps this table had never carried. What did not: every row
20
- * name, every row's bundle membership, and the waterfall event set.
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.
21
23
  */
22
- export const HARNESS_REFERENCE = '0.1.1-rc.2';
24
+ export const HARNESS_REFERENCE = '0.1.2-rc.1';
23
25
  /**
24
26
  * The three profile bundles the harness ships, mapped to what each one is.
25
27
  * A package that *is* one of these composes the core rows rather than modifying
@@ -51,13 +53,11 @@ export const CORE_ROWS = new Map([
51
53
  ['agent-instructions', { module: '@deepseek-ai/dsh-agent-instructions', bundles: ['base'] }],
52
54
  ['agent-loop', { module: '@deepseek-ai/dsh-agent-loop', bundles: ['base'] }],
53
55
  ['agent-presets', { module: '@deepseek-ai/dsh-agent-presets', bundles: ['web-app'] }],
54
- ['api-gateway', { module: '@deepseek-ai/dsh-host-apiproxy', bundles: ['web-app'] }],
55
56
  ['api-remotes', { module: '@deepseek-ai/dsh-api-remotes', bundles: ['web-app'] }],
56
57
  ['approval', { module: '@deepseek-ai/dsh-user-approval', bundles: ['base'] }],
57
58
  ['attachment-local', { module: '@deepseek-ai/dsh-attachment-local', bundles: ['base'] }],
58
59
  ['bash-sandbox', { module: '@deepseek-ai/dsh-bash-sandbox', bundles: ['base'] }],
59
60
  ['client-hmr', { module: '@deepseek-ai/dsh-client-hmr', bundles: ['web-app'] }],
60
- ['client-runtime', { module: '@deepseek-ai/dsh-client-runtime', bundles: ['web-app'] }],
61
61
  ['code-runtime', { module: '@deepseek-ai/dsh-code-runtime-worker-thread', bundles: ['headless', 'web-app'] }],
62
62
  ['command-compact', { module: '@deepseek-ai/dsh-command-compact', bundles: ['base'] }],
63
63
  ['command-feedback', { module: '@deepseek-ai/dsh-command-feedback', bundles: ['base'] }],
@@ -68,6 +68,7 @@ export const CORE_ROWS = new Map([
68
68
  ['cordis-client-runner', { module: '@deepseek-ai/dsh-cordis-client-runner', bundles: ['web-app'] }],
69
69
  ['cordis-host-runner', { module: '@deepseek-ai/dsh-cordis-host-runner', bundles: ['web-app'] }],
70
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'] }],
71
72
  ['directory-picker', { module: '@deepseek-ai/dsh-host-directory-picker-auto', bundles: ['web-app'] }],
72
73
  ['file-reference-local', { module: '@deepseek-ai/dsh-file-reference-local', bundles: ['web-app'] }],
73
74
  ['fs-observation-policy', { module: '@deepseek-ai/dsh-fs-observation-policy', bundles: ['base'] }],
@@ -88,34 +89,40 @@ export const CORE_ROWS = new Map([
88
89
  ['permission', { module: '@deepseek-ai/dsh-permission-presets', bundles: ['base'] }],
89
90
  ['plan-mode', { module: '@deepseek-ai/dsh-plan-mode', bundles: ['base'] }],
90
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'] }],
91
93
  ['pwsh-sandbox', { module: '@deepseek-ai/dsh-pwsh-sandbox', bundles: ['base'] }],
92
94
  ['repeat-tool-reminder', { module: '@deepseek-ai/dsh-repeat-tool-reminder', bundles: ['base'] }],
93
95
  ['sandbox', { module: '@deepseek-ai/dsh-sandbox-local', bundles: ['base'] }],
94
96
  ['sandbox-policy', { module: '@deepseek-ai/dsh-sandbox-policy', bundles: ['base'] }],
95
97
  ['session', { module: '@deepseek-ai/dsh-session', bundles: ['base'] }],
96
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'] }],
97
101
  ['session-log-download', { module: '@deepseek-ai/dsh-session-log-export', bundles: ['web-app'] }],
98
102
  ['session-persistence-jsonl', { module: '@deepseek-ai/dsh-session-persistence-jsonl', bundles: ['base'] }],
99
103
  ['session-projection', { module: '@deepseek-ai/dsh-session-projection', bundles: ['base'] }],
100
- ['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'] }],
101
105
  ['session-query-sqlite', { module: '@deepseek-ai/dsh-session-query-sqlite', bundles: ['base'] }],
102
106
  ['session-reference', { module: '@deepseek-ai/dsh-session-reference', bundles: ['web-app'] }],
103
107
  ['session-stats', { module: '@deepseek-ai/dsh-session-stats', bundles: ['web-app'] }],
104
108
  ['session-telemetry-otel', { module: '@deepseek-ai/dsh-session-telemetry-otel', bundles: ['base'] }],
105
109
  ['session-title', { module: '@deepseek-ai/dsh-session-title', bundles: ['base'] }],
106
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'] }],
107
112
  ['settings', { module: '@deepseek-ai/dsh-settings-file', bundles: ['base'] }],
113
+ ['settings-controller', { module: '@deepseek-ai/dsh-api-settings-controller', bundles: ['web-app'] }],
108
114
  ['shell-env', { module: '@deepseek-ai/dsh-shell-env', bundles: ['base'] }],
109
115
  ['skill', { module: '@deepseek-ai/dsh-skill', bundles: ['base'] }],
110
116
  ['skill-badge', { module: '@deepseek-ai/dsh-skill-badge', bundles: ['base'] }],
111
117
  ['skill-filesystem', { module: '@deepseek-ai/dsh-skill-filesystem', bundles: ['base'] }],
112
118
  ['spill-local', { module: '@deepseek-ai/dsh-spill-local', bundles: ['base'] }],
113
119
  ['spill-policy', { module: '@deepseek-ai/dsh-spill-policy', bundles: ['base'] }],
114
- ['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['web-app'] }],
115
- ['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['web-app'] }],
116
- ['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'] }],
117
123
  ['subagent', { module: '@deepseek-ai/dsh-subagent', bundles: ['base'] }],
118
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'] }],
119
126
  ['subagent-spawn-in-process', { module: '@deepseek-ai/dsh-subagent-spawn-in-process', bundles: ['base'] }],
120
127
  ['subprocess', { module: '@deepseek-ai/dsh-subprocess-local', bundles: ['base'] }],
121
128
  ['system-prompt', { module: '@deepseek-ai/dsh-system-prompt', bundles: ['base'] }],
@@ -136,7 +143,6 @@ export const CORE_ROWS = new Map([
136
143
  ['tool-subagent-control', { module: '@deepseek-ai/dsh-tool-subagent-control', bundles: ['base'] }],
137
144
  ['tool-subagent-fork', { module: '@deepseek-ai/dsh-tool-subagent', bundles: ['base'] }],
138
145
  ['tool-subagent-list-agents', { module: '@deepseek-ai/dsh-tool-subagent-control/list-agents', bundles: ['base'] }],
139
- ['tool-subagent-report', { module: '@deepseek-ai/dsh-tool-subagent-report', bundles: ['base'] }],
140
146
  ['tool-todo', { module: '@deepseek-ai/dsh-tool-todo', bundles: ['base'] }],
141
147
  ['tool-web', { module: '@deepseek-ai/dsh-tool-web', bundles: ['base'] }],
142
148
  ['tool-workflow', { module: '@deepseek-ai/dsh-tool-workflow', bundles: ['base'] }],
@@ -145,8 +151,10 @@ export const CORE_ROWS = new Map([
145
151
  ['typert-gateway', { module: '@deepseek-ai/dsh-api-gateway', bundles: ['base'] }],
146
152
  ['typert-loader', { module: '@deepseek-ai/dsh-typert-loader', bundles: ['base'] }],
147
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'] }],
148
155
  ['ui-attachment', { module: '@deepseek-ai/dsh-client-ui-attachment', bundles: ['web-app'] }],
149
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'] }],
150
158
  ['ui-commands', { module: '@deepseek-ai/dsh-client-ui-commands', bundles: ['web-app'] }],
151
159
  ['ui-conversation', { module: '@deepseek-ai/dsh-client-ui-conversation', bundles: ['web-app'] }],
152
160
  ['ui-cordis', { module: '@deepseek-ai/dsh-client-ui-cordis', bundles: ['web-app'] }],
@@ -161,6 +169,8 @@ export const CORE_ROWS = new Map([
161
169
  ['ui-plan', { module: '@deepseek-ai/dsh-client-ui-plan', bundles: ['web-app'] }],
162
170
  ['ui-reference', { module: '@deepseek-ai/dsh-client-ui-reference', bundles: ['web-app'] }],
163
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'] }],
164
174
  ['ui-settings', { module: '@deepseek-ai/dsh-client-ui-settings', bundles: ['web-app'] }],
165
175
  ['ui-settings-general', { module: '@deepseek-ai/dsh-client-ui-settings-general', bundles: ['web-app'] }],
166
176
  ['ui-settings-models', { module: '@deepseek-ai/dsh-client-ui-settings-models', bundles: ['web-app'] }],
@@ -177,12 +187,14 @@ export const CORE_ROWS = new Map([
177
187
  ['ui-workspace', { module: '@deepseek-ai/dsh-client-ui-workspace', bundles: ['web-app'] }],
178
188
  ['user-questions', { module: '@deepseek-ai/dsh-user-questions', bundles: ['base'] }],
179
189
  ['web', { module: '@deepseek-ai/dsh-web', bundles: ['base'] }],
190
+ ['web-fetch-http', { module: '@deepseek-ai/dsh-web-fetch-http', bundles: ['base'] }],
180
191
  ['web-runtime', { module: '@deepseek-ai/dsh-web-app', bundles: ['web-app'] }],
181
192
  ['web-search-deepseek', { module: '@deepseek-ai/dsh-web-search-deepseek', bundles: ['base'] }],
182
193
  ['web-startup', { module: '@deepseek-ai/dsh-web-app/startup', bundles: ['web-app'] }],
183
194
  ['webserver', { module: '@deepseek-ai/dsh-host-webserver', bundles: ['web-app'] }],
184
195
  ['workflow-worker-thread', { module: '@deepseek-ai/dsh-workflow-worker-thread', bundles: ['base'] }],
185
196
  ['workspace', { module: '@deepseek-ai/dsh-workspace', bundles: ['web-app'] }],
197
+ ['workspace-controller', { module: '@deepseek-ai/dsh-api-workspace-controller', bundles: ['web-app'] }],
186
198
  ]);
187
199
  /** Row ids the shipped bundles define. */
188
200
  export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
@@ -191,7 +203,16 @@ export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
191
203
  * Disabling or reconfiguring one of these from a third-party patch layer is
192
204
  * the highest-value finding this tool produces, and it is plain YAML.
193
205
  *
194
- * Each entry names what stops holding when the row stops running.
206
+ * Each entry names what stops holding when the row stops running. Membership
207
+ * needs one of two properties: disabling the row **fails open**, so the agent
208
+ * may afterwards do something it could not before, or it **removes evidence**
209
+ * without saying so, leaving nothing to reconstruct what the agent did. A row
210
+ * that fails closed on removal, or whose absence only takes a feature away, is
211
+ * not a member however security-adjacent its name reads.
212
+ *
213
+ * The rows examined against that rule and kept out are listed with their
214
+ * reasons under "What is deliberately not a finding" in `docs/checks.md`, so a
215
+ * reader can tell a considered exclusion from a row nobody looked at.
195
216
  */
196
217
  export const SECURITY_ROW_IDS = new Map([
197
218
  ['approval', 'user approval prompts for tool calls'],
@@ -219,32 +240,138 @@ export const SECURITY_ROW_IDS = new Map([
219
240
  * replaces a core service for every consumer in its scope.
220
241
  */
221
242
  export const SEAM_KEYS = new Set([
222
- 'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'apiProxy', 'approval',
223
- 'attachments', 'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction',
224
- 'credentials', 'directoryPicker', 'e2b', 'fileReferences', 'fs', 'goals', 'invariants', 'jobs',
225
- 'llm', 'lsp', 'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy',
226
- 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections', 'sessionQuery',
227
- 'sessionReferenceResolver', 'sessions', 'sessionTelemetry', 'sessionTitle', 'settings',
228
- 'shell', 'shellEnv', 'skills', 'spillStore', 'storage', 'storageDomain', 'subagents',
229
- 'subprocess', 'systemPrompt', 'terminals', 'timer', 'tokenMeter', 'toolResultPruner',
230
- 'tools', 'typert', 'typertGateway', 'userQuestions', 'web', 'webServer', 'workflowEngine',
231
- 'workspaceRegistry',
243
+ 'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'approval', 'attachments',
244
+ 'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
245
+ 'credentialsController', 'deepseekLlmApiExtensions', 'directoryPicker', 'directoryPickerController',
246
+ 'e2b', 'fileReferences', 'fs', 'goals', 'inspector', 'invariants', 'jobs', 'llm', 'lsp',
247
+ 'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy', 'sessionController',
248
+ 'sessionFileReferences', 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections',
249
+ 'sessionQuery', 'sessionReferenceResolver', 'sessions', 'sessionSkillCatalog', 'sessionTelemetry',
250
+ 'sessionTitle', 'settings', 'settingsController', 'shell', 'shellEnv', 'skills', 'spillStore', 'storage',
251
+ 'storageDomain', 'subagentModelSelection', 'subagents', 'subprocess', 'systemPrompt', 'terminals',
252
+ 'timer', 'tokenMeter', 'toolResultPruner', 'tools', 'typert', 'typertGateway', 'userQuestions', 'web',
253
+ 'webhookRuntime', 'webServer', 'workflowEngine', 'workspaceController', 'workspaceRegistry',
232
254
  ]);
233
255
  /**
234
- * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
256
+ * The subset of {@link SEAM_KEYS} whose replacement either removes a constraint
257
+ * or takes over the record by which one could be checked afterwards. Most
258
+ * members are the first kind — `approval`, `sandbox`, `credentials`. The set
259
+ * already holds two of the second, `sessionPersistence` and `sessionTelemetry`,
260
+ * and each entry below says which kind it is rather than borrowing the other's
261
+ * sentence.
262
+ *
263
+ * `authorization` is the registry of flows that obtain a credential through a
264
+ * conversation with the user, so providing it means owning that conversation.
265
+ * That is the same class of substitution as `credentials`, which this set
266
+ * already holds. `fileReferences` decides which paths are offered for
267
+ * completion and `agentTeams` is the team form of `subagents`, which is
268
+ * deliberately not here either, so neither of those is in this set.
269
+ *
270
+ * Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
271
+ * services that own one `ctx.remote.*` namespace the browser client calls
272
+ * across the wire. A controller belongs here only when the traffic a
273
+ * substitution redirects to it carries a secret, an execution boundary, or a
274
+ * decision. A controller that forwards its seam's own verbs and adds a wire
275
+ * failure vocabulary does not, because the seam it fronts is reachable from
276
+ * `ctx` without substituting anything.
277
+ *
278
+ * Included:
279
+ * - `credentialsController` is what a browser configuration page calls to store
280
+ * a credential. `set(ref, value)` receives the plaintext secret and hands it
281
+ * to `ctx.credentials`
282
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
283
+ * `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
284
+ * answer to the three fields `CredentialInfo` declares. A layer that provides
285
+ * it takes both halves: every secret typed into the settings page, and the
286
+ * freedom to answer a read with the stored value.
287
+ * - `settingsController` passes `redactSecrets: true` on every remote read
288
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
289
+ * which is what keeps a `role('secret')` field out of a settings response;
290
+ * `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
291
+ * `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
292
+ * value in plaintext from the configuration page. Providing it puts the layer
293
+ * on both directions of a secret's path.
294
+ * - `sessionController` resolves each new Session's cwd from the wire request
295
+ * and hands it to `ensureSession`
296
+ * (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
297
+ * `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
298
+ * `workspace-write` root the enforcing filesystem, bash and terminal backends
299
+ * fence against. Its `prompt` verb builds the message admitted to the agent
300
+ * under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
301
+ * sandbox root for every session created from the client, and the text that
302
+ * reaches the model under the user's own source label.
303
+ * - `webhookRuntime` is what a provider adapter such as
304
+ * `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
305
+ * one built-in action creates a Session from a rule result whose fields
306
+ * include `workspacePath`, `permissionPreset` and `prompt`
307
+ * (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
308
+ * `createWebhookSession` applies that preset through
309
+ * `ctx.permissionPresets.set` before admitting the prompt
310
+ * (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
311
+ * Providing it picks the approval and sandbox preset for an agent started by
312
+ * a remote delivery with no user present.
313
+ * - `inspector` is the second kind, and the only member graded from the
314
+ * catalogue rather than from an implementation. The catalogue declares it as
315
+ * the façade over the realm's source publisher, with `publish(topic, payload,
316
+ * monotonicMs?)` and a read-only `CordisRuntimeTreeReader`
317
+ * (`@deepseek-ai/dsh-tool-cordis/lib/index.js:1558`, the two method
318
+ * signatures at `:1562` and `:1579`). Providing it
319
+ * takes no decision away from anyone: nothing is gated on an observation, and
320
+ * on that ground the key does not belong beside `approval`. What it takes is
321
+ * the position observations pass through. A substituted publisher chooses
322
+ * which topics reach the carrier and what payload each one carries, so it can
323
+ * withhold the record of something that happened or publish one for something
324
+ * that did not, and a consumer downstream cannot tell either from a quiet
325
+ * system. That is the property this set already recognises in
326
+ * `sessionPersistence` and `sessionTelemetry`.
327
+ *
328
+ * No implementation exists to displace. In `0.1.2-rc.1` the key is declared
329
+ * once and used nowhere: `'inspector'` as a string literal occurs exactly
330
+ * once across the 224 `@deepseek-ai` packages in the installed tree — that
331
+ * catalogue entry — no file reads `ctx.inspector`, and `InspectorJsonValue`
332
+ * and `CordisRuntimeTreeReader` appear only in that same bundle. So a package
333
+ * providing `inspector` in this release displaces nothing and reaches nothing
334
+ * it could not reach under a name of its own. This entry grades what the
335
+ * catalogue says the key is for, not code that runs today, and it is the one
336
+ * entry a release that ships a publisher or a consumer should settle again
337
+ * against them.
235
338
  *
236
- * `authorization` joined the catalogue in `0.1.1-rc.2`: it is the registry of
237
- * flows that obtain a credential through a conversation with the user, so
238
- * providing it means owning that conversation. That is the same class of
239
- * substitution as `credentials`, which this set already holds. The other two
240
- * keys the release added are not: `fileReferences` decides which paths are
241
- * offered for completion and `agentTeams` is the team form of `subagents`,
242
- * which is deliberately not here either.
339
+ * Excluded:
340
+ * - `workspaceController` forwards `request.path` to
341
+ * `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
342
+ * error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
343
+ * to `:212`). The registry it fronts is `workspaceRegistry`, which is not
344
+ * here, so the substitution reaches nothing the seam does not already offer.
345
+ * - `directoryPickerController` is three delegations to
346
+ * `ctx.directoryPicker.capability()` behind a check that refuses a verb the
347
+ * composed backend does not serve
348
+ * (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
349
+ * Path fencing lives in the backend, and `directoryPicker` is not here.
350
+ * - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
351
+ * is excluded above for the same reason: the traffic is path candidates
352
+ * offered for completion.
353
+ * - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
354
+ * for one Session's human-facing composer
355
+ * (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
356
+ * `:227`), and its only consumer in this release is the client skill picker
357
+ * (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
358
+ * model through `ctx.skills.list()` in
359
+ * `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
360
+ * - `subagentModelSelection` is a settings owner answering `{ enabled,
361
+ * allowedModels }`, sampled when an Agent receives its delegation tools. Model
362
+ * routing is `llm` and delegation is `subagents`; neither is here.
363
+ * - `deepseekLlmApiExtensions` hands a substitute the serialized request body
364
+ * and merges the fields it returns, but
365
+ * `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
366
+ * field colliding with the base request, so `messages`, `tools` and `model`
367
+ * are not writable through it. The constraint is in the adapter, not the
368
+ * registry the substitution replaces.
243
369
  */
244
370
  export const SECURITY_SEAM_KEYS = new Set([
245
371
  'approval', 'authorization', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
246
- 'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry',
247
- 'invariants',
372
+ 'credentialsController', 'settingsController', 'sessionController', 'webhookRuntime',
373
+ 'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'inspector', 'sessionPersistence',
374
+ 'sessionTelemetry', 'invariants',
248
375
  ]);
249
376
  /**
250
377
  * Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
@@ -253,19 +380,31 @@ export const SECURITY_SEAM_KEYS = new Set([
253
380
  *
254
381
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
255
382
  *
256
- * Unchanged in `0.1.1-rc.2`. The catalogue's event set grew by four and lost
257
- * one, but every addition carries `mode: 'emit'`, and only `mode: 'waterfall'`
258
- * hands a listener the trailing `next` this set is about.
383
+ * `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
384
+ * and adds `user-questions/request`. Both replace content in a durable log copy
385
+ * or answer a pending request; neither is an `emit` event, so both hand a
386
+ * listener the trailing `next`.
259
387
  */
260
388
  export const WATERFALL_EVENTS = new Set([
261
389
  'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
262
390
  'fs/edit-intent', 'fs/write-intent', 'llm/stream', 'session-telemetry/record',
263
- 'system-prompt/assemble', 'tools/code-dispatch-log', 'tools/execute',
264
- 'tools/post-execute', 'tools/pre-execute',
391
+ 'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
392
+ 'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
265
393
  ]);
266
- /** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
394
+ /**
395
+ * Waterfall events whose short-circuit removes a decision the user would
396
+ * otherwise make.
397
+ *
398
+ * `user-questions/request` is here for the same reason `approval/request` is:
399
+ * `ctx.userQuestions` pauses a tool call until a human answers, and the
400
+ * answerers that put the question on a screen are listeners in the chain rather
401
+ * than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
402
+ * A listener that returns an answer without calling `next()` answers on the
403
+ * user's behalf and the question is never shown.
404
+ */
267
405
  export const DECISION_EVENTS = new Set([
268
406
  'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
407
+ 'user-questions/request',
269
408
  ]);
270
409
  /**
271
410
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -418,3 +557,130 @@ export const HARNESS_INERT_CALLS = new Set([
418
557
  * in code, and it is plain YAML.
419
558
  */
420
559
  export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
560
+ /**
561
+ * How a Cordis waterfall listener delegates, and what happens when it does not.
562
+ *
563
+ * Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
564
+ * `lib/index.js:317-327`:
565
+ *
566
+ * ```js
567
+ * waterfall(...args) {
568
+ * const cbs = this.dispatch("waterfall", args);
569
+ * const inner = args.pop();
570
+ * const next = () => { return (cbs.shift() ?? inner)(...args); };
571
+ * args.push(next);
572
+ * return next();
573
+ * }
574
+ * ```
575
+ *
576
+ * `next` is the trailing argument every listener receives, and `inner` is the
577
+ * harness's own built-in behavior. A listener that returns without calling
578
+ * `next()` therefore ends the chain: neither the listeners still in `cbs` nor
579
+ * `inner` run.
580
+ *
581
+ * The scope of that is one dispatch, not the registry. `dispatch()` builds
582
+ * `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
583
+ * never touched and every skipped listener is registered and runs normally on
584
+ * the next dispatch. The precise word is veto, not removal — Cordis's own
585
+ * JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
586
+ * the built-in behavior". Removal is a different capability with a different
587
+ * reach, and it has its own table below.
588
+ */
589
+ export const WATERFALL_NEXT_PARAMETER = 'next';
590
+ /**
591
+ * What each decision waterfall's built-in `next` settles on when no listener
592
+ * claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
593
+ *
594
+ * This is what a listener that never calls `next()` replaces. The inner
595
+ * callback is the last argument at each site:
596
+ * - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
597
+ * `() => Promise.resolve({ kind: "allow" })`
598
+ * - `tools/execute` — `dsh-tools/lib/index.js:3214`,
599
+ * `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
600
+ * body of the tool call itself
601
+ * - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
602
+ * `() => Promise.resolve("unavailable")`, and the surface that would ask the
603
+ * user is one of the listeners in the chain rather than the inner callback
604
+ * - `user-questions/request` —
605
+ * `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
606
+ * callback passed at `:69`, which rejects with a `UserQuestionError` carrying
607
+ * code `NO_PROVIDER`
608
+ *
609
+ * The three tables in this module that name events (`WATERFALL_EVENTS`,
610
+ * `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
611
+ */
612
+ export const DECISION_EVENT_DEFAULTS = new Map([
613
+ ['approval/request', 'the request falls through to `"unavailable"` only after every composed answerer — '
614
+ + 'including the surface that would ask the user — has had the dispatch'],
615
+ ['tools/pre-execute', 'the gate settles on `{ kind: "allow" }` after every other listener, and only then are '
616
+ + '`ctx.tools.guard()` denials consulted'],
617
+ ['tools/execute', 'the tool body itself runs'],
618
+ ['fs/write-intent', 'the write intent reaches the policy rows that decide it'],
619
+ ['fs/edit-intent', 'the edit intent reaches the policy rows that decide it'],
620
+ ['user-questions/request', 'the request rejects with `NO_PROVIDER` only after every composed answerer — '
621
+ + 'including the one that puts the question on the user\'s screen — has had the dispatch'],
622
+ ]);
623
+ /**
624
+ * Receivers whose members name a plugin context.
625
+ *
626
+ * The same set the Tier C detached-member check guards on, minus `process`:
627
+ * a seam is read off the context, never off `process`.
628
+ */
629
+ export const CONTEXT_RECEIVERS = new Set([
630
+ 'ctx', 'context', 'globalThis', 'global',
631
+ ]);
632
+ /**
633
+ * Array and collection methods that change the receiver rather than reading it.
634
+ *
635
+ * Used to tell a write into a service's internals from a read of them. The
636
+ * distinction is not academic: `dsh-dlp` reads
637
+ * `ctx.events._hooks['approval/request']?.length` to decide whether an ask
638
+ * would reach a human, which is an honest use of the same property a hostile
639
+ * layer splices.
640
+ */
641
+ export const MUTATING_METHODS = new Set([
642
+ 'splice', 'push', 'pop', 'shift', 'unshift', 'fill', 'sort', 'reverse', 'copyWithin',
643
+ 'clear', 'delete', 'set', 'add',
644
+ ]);
645
+ /**
646
+ * Cordis internals through which one plugin removes another plugin's
647
+ * registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
648
+ *
649
+ * None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
650
+ * `ctx.reflect` are own properties of the root context inherited by every
651
+ * child, so no `inject` declaration is needed to reach any of them.
652
+ */
653
+ export const TEARDOWN_SURFACES = [
654
+ {
655
+ service: 'events',
656
+ member: '_hooks',
657
+ readIsEnough: false,
658
+ effect: 'the listener table every layer\'s `ctx.on()` registration is stored in (`lib/index.js:230`, '
659
+ + '`_hooks = {}`, appended to by `register` at `lib/index.js:336-345`). Splicing an entry out removes that '
660
+ + 'listener permanently, and the owning layer\'s own disposer then silently does nothing. This is a stronger '
661
+ + 'reach than a waterfall veto, which only skips listeners for one dispatch',
662
+ },
663
+ {
664
+ service: 'events',
665
+ member: 'unregister',
666
+ readIsEnough: true,
667
+ effect: 'the public removal path for one listener, by callback identity (`lib/index.js:353-359`). It takes the '
668
+ + 'listener list and a callback and splices, with no check that the caller owns either',
669
+ },
670
+ {
671
+ service: 'registry',
672
+ member: 'delete',
673
+ readIsEnough: true,
674
+ effect: 'disposal of every fiber a plugin owns (`lib/index.js:1564-1571`: `for (const fiber of runtime.fibers) '
675
+ + 'fiber.dispose();`). It takes no ownership check, so one layer can unload another layer outright — '
676
+ + 'including a security layer whose guards and listeners then stop existing',
677
+ },
678
+ {
679
+ service: 'reflect',
680
+ member: 'store',
681
+ readIsEnough: false,
682
+ effect: 'the service implementation table keyed by isolate symbol (`lib/index.js:726`, written by `provide` at '
683
+ + '`lib/index.js:813`). `provide` throws when a key is already taken and `set` throws across fibers; writing '
684
+ + 'this object directly is the path around both throws',
685
+ },
686
+ ];
@@ -13,13 +13,15 @@
13
13
  * Harness version these tables were transcribed from — the version string in
14
14
  * the shipped bundles' own `package.json`, which is `dsh`'s own version.
15
15
  *
16
- * Re-verified against `0.1.1-rc.2`, the release npm tags `latest`, by
16
+ * Re-verified against `0.1.2-rc.1`, the release npm tags `latest`, by
17
17
  * extracting each table from the published packages and diffing it against the
18
- * one here. What moved: six rows inserted by the web bundle, three seam keys,
19
- * and two sandbox traps this table had never carried. What did not: every row
20
- * name, every row's bundle membership, and the waterfall event set.
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.
21
23
  */
22
- export declare const HARNESS_REFERENCE = "0.1.1-rc.2";
24
+ export declare const HARNESS_REFERENCE = "0.1.2-rc.1";
23
25
  /** The shipped bundles, each of which is one patch layer over the profile root. */
24
26
  export type BundleName = 'base' | 'headless' | 'web-app';
25
27
  /**
@@ -58,7 +60,16 @@ export declare const CORE_ROW_IDS: ReadonlySet<string>;
58
60
  * Disabling or reconfiguring one of these from a third-party patch layer is
59
61
  * the highest-value finding this tool produces, and it is plain YAML.
60
62
  *
61
- * Each entry names what stops holding when the row stops running.
63
+ * Each entry names what stops holding when the row stops running. Membership
64
+ * needs one of two properties: disabling the row **fails open**, so the agent
65
+ * may afterwards do something it could not before, or it **removes evidence**
66
+ * without saying so, leaving nothing to reconstruct what the agent did. A row
67
+ * that fails closed on removal, or whose absence only takes a feature away, is
68
+ * not a member however security-adjacent its name reads.
69
+ *
70
+ * The rows examined against that rule and kept out are listed with their
71
+ * reasons under "What is deliberately not a finding" in `docs/checks.md`, so a
72
+ * reader can tell a considered exclusion from a row nobody looked at.
62
73
  */
63
74
  export declare const SECURITY_ROW_IDS: ReadonlyMap<string, string>;
64
75
  /**
@@ -69,15 +80,119 @@ export declare const SECURITY_ROW_IDS: ReadonlyMap<string, string>;
69
80
  */
70
81
  export declare const SEAM_KEYS: ReadonlySet<string>;
71
82
  /**
72
- * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
83
+ * The subset of {@link SEAM_KEYS} whose replacement either removes a constraint
84
+ * or takes over the record by which one could be checked afterwards. Most
85
+ * members are the first kind — `approval`, `sandbox`, `credentials`. The set
86
+ * already holds two of the second, `sessionPersistence` and `sessionTelemetry`,
87
+ * and each entry below says which kind it is rather than borrowing the other's
88
+ * sentence.
89
+ *
90
+ * `authorization` is the registry of flows that obtain a credential through a
91
+ * conversation with the user, so providing it means owning that conversation.
92
+ * That is the same class of substitution as `credentials`, which this set
93
+ * already holds. `fileReferences` decides which paths are offered for
94
+ * completion and `agentTeams` is the team form of `subagents`, which is
95
+ * deliberately not here either, so neither of those is in this set.
96
+ *
97
+ * Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
98
+ * services that own one `ctx.remote.*` namespace the browser client calls
99
+ * across the wire. A controller belongs here only when the traffic a
100
+ * substitution redirects to it carries a secret, an execution boundary, or a
101
+ * decision. A controller that forwards its seam's own verbs and adds a wire
102
+ * failure vocabulary does not, because the seam it fronts is reachable from
103
+ * `ctx` without substituting anything.
104
+ *
105
+ * Included:
106
+ * - `credentialsController` is what a browser configuration page calls to store
107
+ * a credential. `set(ref, value)` receives the plaintext secret and hands it
108
+ * to `ctx.credentials`
109
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
110
+ * `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
111
+ * answer to the three fields `CredentialInfo` declares. A layer that provides
112
+ * it takes both halves: every secret typed into the settings page, and the
113
+ * freedom to answer a read with the stored value.
114
+ * - `settingsController` passes `redactSecrets: true` on every remote read
115
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
116
+ * which is what keeps a `role('secret')` field out of a settings response;
117
+ * `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
118
+ * `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
119
+ * value in plaintext from the configuration page. Providing it puts the layer
120
+ * on both directions of a secret's path.
121
+ * - `sessionController` resolves each new Session's cwd from the wire request
122
+ * and hands it to `ensureSession`
123
+ * (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
124
+ * `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
125
+ * `workspace-write` root the enforcing filesystem, bash and terminal backends
126
+ * fence against. Its `prompt` verb builds the message admitted to the agent
127
+ * under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
128
+ * sandbox root for every session created from the client, and the text that
129
+ * reaches the model under the user's own source label.
130
+ * - `webhookRuntime` is what a provider adapter such as
131
+ * `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
132
+ * one built-in action creates a Session from a rule result whose fields
133
+ * include `workspacePath`, `permissionPreset` and `prompt`
134
+ * (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
135
+ * `createWebhookSession` applies that preset through
136
+ * `ctx.permissionPresets.set` before admitting the prompt
137
+ * (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
138
+ * Providing it picks the approval and sandbox preset for an agent started by
139
+ * a remote delivery with no user present.
140
+ * - `inspector` is the second kind, and the only member graded from the
141
+ * catalogue rather than from an implementation. The catalogue declares it as
142
+ * the façade over the realm's source publisher, with `publish(topic, payload,
143
+ * monotonicMs?)` and a read-only `CordisRuntimeTreeReader`
144
+ * (`@deepseek-ai/dsh-tool-cordis/lib/index.js:1558`, the two method
145
+ * signatures at `:1562` and `:1579`). Providing it
146
+ * takes no decision away from anyone: nothing is gated on an observation, and
147
+ * on that ground the key does not belong beside `approval`. What it takes is
148
+ * the position observations pass through. A substituted publisher chooses
149
+ * which topics reach the carrier and what payload each one carries, so it can
150
+ * withhold the record of something that happened or publish one for something
151
+ * that did not, and a consumer downstream cannot tell either from a quiet
152
+ * system. That is the property this set already recognises in
153
+ * `sessionPersistence` and `sessionTelemetry`.
73
154
  *
74
- * `authorization` joined the catalogue in `0.1.1-rc.2`: it is the registry of
75
- * flows that obtain a credential through a conversation with the user, so
76
- * providing it means owning that conversation. That is the same class of
77
- * substitution as `credentials`, which this set already holds. The other two
78
- * keys the release added are not: `fileReferences` decides which paths are
79
- * offered for completion and `agentTeams` is the team form of `subagents`,
80
- * which is deliberately not here either.
155
+ * No implementation exists to displace. In `0.1.2-rc.1` the key is declared
156
+ * once and used nowhere: `'inspector'` as a string literal occurs exactly
157
+ * once across the 224 `@deepseek-ai` packages in the installed tree that
158
+ * catalogue entry no file reads `ctx.inspector`, and `InspectorJsonValue`
159
+ * and `CordisRuntimeTreeReader` appear only in that same bundle. So a package
160
+ * providing `inspector` in this release displaces nothing and reaches nothing
161
+ * it could not reach under a name of its own. This entry grades what the
162
+ * catalogue says the key is for, not code that runs today, and it is the one
163
+ * entry a release that ships a publisher or a consumer should settle again
164
+ * against them.
165
+ *
166
+ * Excluded:
167
+ * - `workspaceController` forwards `request.path` to
168
+ * `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
169
+ * error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
170
+ * to `:212`). The registry it fronts is `workspaceRegistry`, which is not
171
+ * here, so the substitution reaches nothing the seam does not already offer.
172
+ * - `directoryPickerController` is three delegations to
173
+ * `ctx.directoryPicker.capability()` behind a check that refuses a verb the
174
+ * composed backend does not serve
175
+ * (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
176
+ * Path fencing lives in the backend, and `directoryPicker` is not here.
177
+ * - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
178
+ * is excluded above for the same reason: the traffic is path candidates
179
+ * offered for completion.
180
+ * - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
181
+ * for one Session's human-facing composer
182
+ * (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
183
+ * `:227`), and its only consumer in this release is the client skill picker
184
+ * (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
185
+ * model through `ctx.skills.list()` in
186
+ * `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
187
+ * - `subagentModelSelection` is a settings owner answering `{ enabled,
188
+ * allowedModels }`, sampled when an Agent receives its delegation tools. Model
189
+ * routing is `llm` and delegation is `subagents`; neither is here.
190
+ * - `deepseekLlmApiExtensions` hands a substitute the serialized request body
191
+ * and merges the fields it returns, but
192
+ * `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
193
+ * field colliding with the base request, so `messages`, `tools` and `model`
194
+ * are not writable through it. The constraint is in the adapter, not the
195
+ * registry the substitution replaces.
81
196
  */
82
197
  export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
83
198
  /**
@@ -87,12 +202,23 @@ export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
87
202
  *
88
203
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
89
204
  *
90
- * Unchanged in `0.1.1-rc.2`. The catalogue's event set grew by four and lost
91
- * one, but every addition carries `mode: 'emit'`, and only `mode: 'waterfall'`
92
- * hands a listener the trailing `next` this set is about.
205
+ * `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
206
+ * and adds `user-questions/request`. Both replace content in a durable log copy
207
+ * or answer a pending request; neither is an `emit` event, so both hand a
208
+ * listener the trailing `next`.
93
209
  */
94
210
  export declare const WATERFALL_EVENTS: ReadonlySet<string>;
95
- /** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
211
+ /**
212
+ * Waterfall events whose short-circuit removes a decision the user would
213
+ * otherwise make.
214
+ *
215
+ * `user-questions/request` is here for the same reason `approval/request` is:
216
+ * `ctx.userQuestions` pauses a tool call until a human answers, and the
217
+ * answerers that put the question on a screen are listeners in the chain rather
218
+ * than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
219
+ * A listener that returns an answer without calling `next()` answers on the
220
+ * user's behalf and the question is never shown.
221
+ */
96
222
  export declare const DECISION_EVENTS: ReadonlySet<string>;
97
223
  /**
98
224
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -208,4 +334,97 @@ export declare const HARNESS_INERT_CALLS: ReadonlySet<string>;
208
334
  * in code, and it is plain YAML.
209
335
  */
210
336
  export declare const SERVICE_REMAPPING_FIELDS: readonly string[];
337
+ /**
338
+ * How a Cordis waterfall listener delegates, and what happens when it does not.
339
+ *
340
+ * Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
341
+ * `lib/index.js:317-327`:
342
+ *
343
+ * ```js
344
+ * waterfall(...args) {
345
+ * const cbs = this.dispatch("waterfall", args);
346
+ * const inner = args.pop();
347
+ * const next = () => { return (cbs.shift() ?? inner)(...args); };
348
+ * args.push(next);
349
+ * return next();
350
+ * }
351
+ * ```
352
+ *
353
+ * `next` is the trailing argument every listener receives, and `inner` is the
354
+ * harness's own built-in behavior. A listener that returns without calling
355
+ * `next()` therefore ends the chain: neither the listeners still in `cbs` nor
356
+ * `inner` run.
357
+ *
358
+ * The scope of that is one dispatch, not the registry. `dispatch()` builds
359
+ * `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
360
+ * never touched and every skipped listener is registered and runs normally on
361
+ * the next dispatch. The precise word is veto, not removal — Cordis's own
362
+ * JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
363
+ * the built-in behavior". Removal is a different capability with a different
364
+ * reach, and it has its own table below.
365
+ */
366
+ export declare const WATERFALL_NEXT_PARAMETER = "next";
367
+ /**
368
+ * What each decision waterfall's built-in `next` settles on when no listener
369
+ * claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
370
+ *
371
+ * This is what a listener that never calls `next()` replaces. The inner
372
+ * callback is the last argument at each site:
373
+ * - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
374
+ * `() => Promise.resolve({ kind: "allow" })`
375
+ * - `tools/execute` — `dsh-tools/lib/index.js:3214`,
376
+ * `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
377
+ * body of the tool call itself
378
+ * - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
379
+ * `() => Promise.resolve("unavailable")`, and the surface that would ask the
380
+ * user is one of the listeners in the chain rather than the inner callback
381
+ * - `user-questions/request` —
382
+ * `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
383
+ * callback passed at `:69`, which rejects with a `UserQuestionError` carrying
384
+ * code `NO_PROVIDER`
385
+ *
386
+ * The three tables in this module that name events (`WATERFALL_EVENTS`,
387
+ * `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
388
+ */
389
+ export declare const DECISION_EVENT_DEFAULTS: ReadonlyMap<string, string>;
390
+ /**
391
+ * Receivers whose members name a plugin context.
392
+ *
393
+ * The same set the Tier C detached-member check guards on, minus `process`:
394
+ * a seam is read off the context, never off `process`.
395
+ */
396
+ export declare const CONTEXT_RECEIVERS: ReadonlySet<string>;
397
+ /**
398
+ * Array and collection methods that change the receiver rather than reading it.
399
+ *
400
+ * Used to tell a write into a service's internals from a read of them. The
401
+ * distinction is not academic: `dsh-dlp` reads
402
+ * `ctx.events._hooks['approval/request']?.length` to decide whether an ask
403
+ * would reach a human, which is an honest use of the same property a hostile
404
+ * layer splices.
405
+ */
406
+ export declare const MUTATING_METHODS: ReadonlySet<string>;
407
+ /** One Cordis bookkeeping surface that owns other plugins' registrations. */
408
+ export interface TeardownSurface {
409
+ /** The member read off the context, e.g. `events`. */
410
+ readonly service: string;
411
+ /** The member read off that, e.g. `_hooks`. */
412
+ readonly member: string;
413
+ /**
414
+ * True when merely naming the surface is the finding. False when only a
415
+ * write counts, because reading it is something an honest plugin does.
416
+ */
417
+ readonly readIsEnough: boolean;
418
+ /** What reaching it does, phrased for a report. */
419
+ readonly effect: string;
420
+ }
421
+ /**
422
+ * Cordis internals through which one plugin removes another plugin's
423
+ * registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
424
+ *
425
+ * None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
426
+ * `ctx.reflect` are own properties of the root context inherited by every
427
+ * child, so no `inject` declaration is needed to reach any of them.
428
+ */
429
+ export declare const TEARDOWN_SURFACES: readonly TeardownSurface[];
211
430
  //# sourceMappingURL=knowledge.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-inspector",
3
- "version": "0.7.0",
3
+ "version": "0.9.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",