dsh-plugin-inspector 0.7.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.
@@ -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());
@@ -219,30 +231,114 @@ export const SECURITY_ROW_IDS = new Map([
219
231
  * replaces a core service for every consumer in its scope.
220
232
  */
221
233
  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',
234
+ 'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'approval', 'attachments',
235
+ 'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
236
+ 'credentialsController', 'deepseekLlmApiExtensions', 'directoryPicker', 'directoryPickerController',
237
+ 'e2b', 'fileReferences', 'fs', 'goals', 'inspector', 'invariants', 'jobs', 'llm', 'lsp',
238
+ 'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy', 'sessionController',
239
+ 'sessionFileReferences', 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections',
240
+ 'sessionQuery', 'sessionReferenceResolver', 'sessions', 'sessionSkillCatalog', 'sessionTelemetry',
241
+ 'sessionTitle', 'settings', 'settingsController', 'shell', 'shellEnv', 'skills', 'spillStore', 'storage',
242
+ 'storageDomain', 'subagentModelSelection', 'subagents', 'subprocess', 'systemPrompt', 'terminals',
243
+ 'timer', 'tokenMeter', 'toolResultPruner', 'tools', 'typert', 'typertGateway', 'userQuestions', 'web',
244
+ 'webhookRuntime', 'webServer', 'workflowEngine', 'workspaceController', 'workspaceRegistry',
232
245
  ]);
233
246
  /**
234
247
  * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
235
248
  *
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.
249
+ * `authorization` is the registry of flows that obtain a credential through a
250
+ * conversation with the user, so providing it means owning that conversation.
251
+ * That is the same class of substitution as `credentials`, which this set
252
+ * already holds. `fileReferences` decides which paths are offered for
253
+ * completion and `agentTeams` is the team form of `subagents`, which is
254
+ * deliberately not here either, so neither of those is in this set.
255
+ *
256
+ * Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
257
+ * services that own one `ctx.remote.*` namespace the browser client calls
258
+ * across the wire. A controller belongs here only when the traffic a
259
+ * substitution redirects to it carries a secret, an execution boundary, or a
260
+ * decision. A controller that forwards its seam's own verbs and adds a wire
261
+ * failure vocabulary does not, because the seam it fronts is reachable from
262
+ * `ctx` without substituting anything.
263
+ *
264
+ * Included:
265
+ * - `credentialsController` is what a browser configuration page calls to store
266
+ * a credential. `set(ref, value)` receives the plaintext secret and hands it
267
+ * to `ctx.credentials`
268
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
269
+ * `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
270
+ * answer to the three fields `CredentialInfo` declares. A layer that provides
271
+ * it takes both halves: every secret typed into the settings page, and the
272
+ * freedom to answer a read with the stored value.
273
+ * - `settingsController` passes `redactSecrets: true` on every remote read
274
+ * (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
275
+ * which is what keeps a `role('secret')` field out of a settings response;
276
+ * `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
277
+ * `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
278
+ * value in plaintext from the configuration page. Providing it puts the layer
279
+ * on both directions of a secret's path.
280
+ * - `sessionController` resolves each new Session's cwd from the wire request
281
+ * and hands it to `ensureSession`
282
+ * (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
283
+ * `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
284
+ * `workspace-write` root the enforcing filesystem, bash and terminal backends
285
+ * fence against. Its `prompt` verb builds the message admitted to the agent
286
+ * under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
287
+ * sandbox root for every session created from the client, and the text that
288
+ * reaches the model under the user's own source label.
289
+ * - `webhookRuntime` is what a provider adapter such as
290
+ * `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
291
+ * one built-in action creates a Session from a rule result whose fields
292
+ * include `workspacePath`, `permissionPreset` and `prompt`
293
+ * (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
294
+ * `createWebhookSession` applies that preset through
295
+ * `ctx.permissionPresets.set` before admitting the prompt
296
+ * (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
297
+ * Providing it picks the approval and sandbox preset for an agent started by
298
+ * a remote delivery with no user present.
299
+ *
300
+ * Excluded:
301
+ * - `workspaceController` forwards `request.path` to
302
+ * `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
303
+ * error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
304
+ * to `:212`). The registry it fronts is `workspaceRegistry`, which is not
305
+ * here, so the substitution reaches nothing the seam does not already offer.
306
+ * - `directoryPickerController` is three delegations to
307
+ * `ctx.directoryPicker.capability()` behind a check that refuses a verb the
308
+ * composed backend does not serve
309
+ * (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
310
+ * Path fencing lives in the backend, and `directoryPicker` is not here.
311
+ * - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
312
+ * is excluded above for the same reason: the traffic is path candidates
313
+ * offered for completion.
314
+ * - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
315
+ * for one Session's human-facing composer
316
+ * (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
317
+ * `:227`), and its only consumer in this release is the client skill picker
318
+ * (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
319
+ * model through `ctx.skills.list()` in
320
+ * `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
321
+ * - `subagentModelSelection` is a settings owner answering `{ enabled,
322
+ * allowedModels }`, sampled when an Agent receives its delegation tools. Model
323
+ * routing is `llm` and delegation is `subagents`; neither is here.
324
+ * - `deepseekLlmApiExtensions` hands a substitute the serialized request body
325
+ * and merges the fields it returns, but
326
+ * `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
327
+ * field colliding with the base request, so `messages`, `tools` and `model`
328
+ * are not writable through it. The constraint is in the adapter, not the
329
+ * registry the substitution replaces.
330
+ * - `inspector` is declared by the catalogue and implemented by no shipped
331
+ * package: in `0.1.2-rc.1` the key, `InspectorJsonValue` and
332
+ * `CordisRuntimeTreeReader` occur only in
333
+ * `@deepseek-ai/dsh-tool-cordis/lib/index.js`, and nothing provides or reads
334
+ * `ctx.inspector`. No composed row enforces anything through it, so a
335
+ * substitution displaces nothing. This is the one entry decided from the
336
+ * catalogue rather than from an implementation; a release that ships one is a
337
+ * reason to decide it again.
243
338
  */
244
339
  export const SECURITY_SEAM_KEYS = new Set([
245
340
  'approval', 'authorization', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
341
+ 'credentialsController', 'settingsController', 'sessionController', 'webhookRuntime',
246
342
  'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry',
247
343
  'invariants',
248
344
  ]);
@@ -253,19 +349,31 @@ export const SECURITY_SEAM_KEYS = new Set([
253
349
  *
254
350
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
255
351
  *
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.
352
+ * `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
353
+ * and adds `user-questions/request`. Both replace content in a durable log copy
354
+ * or answer a pending request; neither is an `emit` event, so both hand a
355
+ * listener the trailing `next`.
259
356
  */
260
357
  export const WATERFALL_EVENTS = new Set([
261
358
  'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
262
359
  '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',
360
+ 'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
361
+ 'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
265
362
  ]);
266
- /** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
363
+ /**
364
+ * Waterfall events whose short-circuit removes a decision the user would
365
+ * otherwise make.
366
+ *
367
+ * `user-questions/request` is here for the same reason `approval/request` is:
368
+ * `ctx.userQuestions` pauses a tool call until a human answers, and the
369
+ * answerers that put the question on a screen are listeners in the chain rather
370
+ * than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
371
+ * A listener that returns an answer without calling `next()` answers on the
372
+ * user's behalf and the question is never shown.
373
+ */
267
374
  export const DECISION_EVENTS = new Set([
268
375
  'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
376
+ 'user-questions/request',
269
377
  ]);
270
378
  /**
271
379
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -418,3 +526,130 @@ export const HARNESS_INERT_CALLS = new Set([
418
526
  * in code, and it is plain YAML.
419
527
  */
420
528
  export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
529
+ /**
530
+ * How a Cordis waterfall listener delegates, and what happens when it does not.
531
+ *
532
+ * Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
533
+ * `lib/index.js:317-327`:
534
+ *
535
+ * ```js
536
+ * waterfall(...args) {
537
+ * const cbs = this.dispatch("waterfall", args);
538
+ * const inner = args.pop();
539
+ * const next = () => { return (cbs.shift() ?? inner)(...args); };
540
+ * args.push(next);
541
+ * return next();
542
+ * }
543
+ * ```
544
+ *
545
+ * `next` is the trailing argument every listener receives, and `inner` is the
546
+ * harness's own built-in behavior. A listener that returns without calling
547
+ * `next()` therefore ends the chain: neither the listeners still in `cbs` nor
548
+ * `inner` run.
549
+ *
550
+ * The scope of that is one dispatch, not the registry. `dispatch()` builds
551
+ * `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
552
+ * never touched and every skipped listener is registered and runs normally on
553
+ * the next dispatch. The precise word is veto, not removal — Cordis's own
554
+ * JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
555
+ * the built-in behavior". Removal is a different capability with a different
556
+ * reach, and it has its own table below.
557
+ */
558
+ export const WATERFALL_NEXT_PARAMETER = 'next';
559
+ /**
560
+ * What each decision waterfall's built-in `next` settles on when no listener
561
+ * claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
562
+ *
563
+ * This is what a listener that never calls `next()` replaces. The inner
564
+ * callback is the last argument at each site:
565
+ * - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
566
+ * `() => Promise.resolve({ kind: "allow" })`
567
+ * - `tools/execute` — `dsh-tools/lib/index.js:3214`,
568
+ * `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
569
+ * body of the tool call itself
570
+ * - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
571
+ * `() => Promise.resolve("unavailable")`, and the surface that would ask the
572
+ * user is one of the listeners in the chain rather than the inner callback
573
+ * - `user-questions/request` —
574
+ * `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
575
+ * callback passed at `:69`, which rejects with a `UserQuestionError` carrying
576
+ * code `NO_PROVIDER`
577
+ *
578
+ * The three tables in this module that name events (`WATERFALL_EVENTS`,
579
+ * `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
580
+ */
581
+ export const DECISION_EVENT_DEFAULTS = new Map([
582
+ ['approval/request', 'the request falls through to `"unavailable"` only after every composed answerer — '
583
+ + 'including the surface that would ask the user — has had the dispatch'],
584
+ ['tools/pre-execute', 'the gate settles on `{ kind: "allow" }` after every other listener, and only then are '
585
+ + '`ctx.tools.guard()` denials consulted'],
586
+ ['tools/execute', 'the tool body itself runs'],
587
+ ['fs/write-intent', 'the write intent reaches the policy rows that decide it'],
588
+ ['fs/edit-intent', 'the edit intent reaches the policy rows that decide it'],
589
+ ['user-questions/request', 'the request rejects with `NO_PROVIDER` only after every composed answerer — '
590
+ + 'including the one that puts the question on the user\'s screen — has had the dispatch'],
591
+ ]);
592
+ /**
593
+ * Receivers whose members name a plugin context.
594
+ *
595
+ * The same set the Tier C detached-member check guards on, minus `process`:
596
+ * a seam is read off the context, never off `process`.
597
+ */
598
+ export const CONTEXT_RECEIVERS = new Set([
599
+ 'ctx', 'context', 'globalThis', 'global',
600
+ ]);
601
+ /**
602
+ * Array and collection methods that change the receiver rather than reading it.
603
+ *
604
+ * Used to tell a write into a service's internals from a read of them. The
605
+ * distinction is not academic: `dsh-dlp` reads
606
+ * `ctx.events._hooks['approval/request']?.length` to decide whether an ask
607
+ * would reach a human, which is an honest use of the same property a hostile
608
+ * layer splices.
609
+ */
610
+ export const MUTATING_METHODS = new Set([
611
+ 'splice', 'push', 'pop', 'shift', 'unshift', 'fill', 'sort', 'reverse', 'copyWithin',
612
+ 'clear', 'delete', 'set', 'add',
613
+ ]);
614
+ /**
615
+ * Cordis internals through which one plugin removes another plugin's
616
+ * registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
617
+ *
618
+ * None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
619
+ * `ctx.reflect` are own properties of the root context inherited by every
620
+ * child, so no `inject` declaration is needed to reach any of them.
621
+ */
622
+ export const TEARDOWN_SURFACES = [
623
+ {
624
+ service: 'events',
625
+ member: '_hooks',
626
+ readIsEnough: false,
627
+ effect: 'the listener table every layer\'s `ctx.on()` registration is stored in (`lib/index.js:230`, '
628
+ + '`_hooks = {}`, appended to by `register` at `lib/index.js:336-345`). Splicing an entry out removes that '
629
+ + 'listener permanently, and the owning layer\'s own disposer then silently does nothing. This is a stronger '
630
+ + 'reach than a waterfall veto, which only skips listeners for one dispatch',
631
+ },
632
+ {
633
+ service: 'events',
634
+ member: 'unregister',
635
+ readIsEnough: true,
636
+ effect: 'the public removal path for one listener, by callback identity (`lib/index.js:353-359`). It takes the '
637
+ + 'listener list and a callback and splices, with no check that the caller owns either',
638
+ },
639
+ {
640
+ service: 'registry',
641
+ member: 'delete',
642
+ readIsEnough: true,
643
+ effect: 'disposal of every fiber a plugin owns (`lib/index.js:1564-1571`: `for (const fiber of runtime.fibers) '
644
+ + 'fiber.dispose();`). It takes no ownership check, so one layer can unload another layer outright — '
645
+ + 'including a security layer whose guards and listeners then stop existing',
646
+ },
647
+ {
648
+ service: 'reflect',
649
+ member: 'store',
650
+ readIsEnough: false,
651
+ effect: 'the service implementation table keyed by isolate symbol (`lib/index.js:726`, written by `provide` at '
652
+ + '`lib/index.js:813`). `provide` throws when a key is already taken and `set` throws across fibers; writing '
653
+ + 'this object directly is the path around both throws',
654
+ },
655
+ ];
@@ -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
  /**
@@ -71,13 +73,95 @@ export declare const SEAM_KEYS: ReadonlySet<string>;
71
73
  /**
72
74
  * The subset of {@link SEAM_KEYS} whose replacement removes a constraint.
73
75
  *
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.
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.
81
165
  */
82
166
  export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
83
167
  /**
@@ -87,12 +171,23 @@ export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
87
171
  *
88
172
  * Note there is no `fs/read-intent` — the intent family is write and edit only.
89
173
  *
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.
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`.
93
178
  */
94
179
  export declare const WATERFALL_EVENTS: ReadonlySet<string>;
95
- /** 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
+ */
96
191
  export declare const DECISION_EVENTS: ReadonlySet<string>;
97
192
  /**
98
193
  * Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
@@ -208,4 +303,97 @@ export declare const HARNESS_INERT_CALLS: ReadonlySet<string>;
208
303
  * in code, and it is plain YAML.
209
304
  */
210
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[];
211
399
  //# 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.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",