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.
- package/lib/checks/tier-a.js +69 -1
- package/lib/checks/tier-b.js +290 -1
- package/lib/knowledge.js +270 -35
- package/lib/types/knowledge.d.ts +204 -16
- package/package.json +1 -1
package/lib/checks/tier-a.js
CHANGED
|
@@ -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),
|
package/lib/checks/tier-b.js
CHANGED
|
@@ -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.
|
|
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:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
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: ['
|
|
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: ['
|
|
115
|
-
['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['
|
|
116
|
-
['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['
|
|
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', '
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'
|
|
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`
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
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
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
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/
|
|
264
|
-
'tools/
|
|
360
|
+
'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
|
|
361
|
+
'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
|
|
265
362
|
]);
|
|
266
|
-
/**
|
|
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
|
+
];
|
package/lib/types/knowledge.d.ts
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.
|
|
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:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
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`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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",
|