dsh-plugin-inspector 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/lib/checks/tier-a.js +69 -1
- package/lib/checks/tier-b.js +290 -1
- package/lib/knowledge.js +305 -39
- package/lib/types/knowledge.d.ts +237 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,20 @@ Findings are tiered by how much you should trust them:
|
|
|
79
79
|
|
|
80
80
|
[Every check, by tier →](https://charlotten7.github.io/dsh-plugin-inspector/checks.html)
|
|
81
81
|
|
|
82
|
+
## What it deliberately does not flag
|
|
83
|
+
|
|
84
|
+
A `critical` is only worth reading if it is rare, so the two tables that decide one — the core rows
|
|
85
|
+
A2 treats as security-relevant, and the capability seams A23, B1 and B15 do — are kept small on a
|
|
86
|
+
stated rule: a row is in when disabling it **fails open** or **silently removes evidence**. A row
|
|
87
|
+
that fails closed when removed is out, however security-adjacent its name reads, and so is one whose
|
|
88
|
+
absence takes a feature away and grants nothing.
|
|
89
|
+
|
|
90
|
+
The rows checked against that rule and kept out are named with their reasons, so you can tell "we
|
|
91
|
+
checked and it does not qualify" from "we never looked". Excluded is not unreported: disabling any
|
|
92
|
+
core row is still an A3 finding.
|
|
93
|
+
|
|
94
|
+
[What is deliberately not a finding →](https://charlotten7.github.io/dsh-plugin-inspector/checks.html#what-is-deliberately-not-a-finding)
|
|
95
|
+
|
|
82
96
|
## The ceiling
|
|
83
97
|
|
|
84
98
|
**This is not a malware scanner and it cannot be one.** Capability is decidable from source;
|
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());
|
|
@@ -191,7 +203,16 @@ export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
|
|
|
191
203
|
* Disabling or reconfiguring one of these from a third-party patch layer is
|
|
192
204
|
* the highest-value finding this tool produces, and it is plain YAML.
|
|
193
205
|
*
|
|
194
|
-
* Each entry names what stops holding when the row stops running.
|
|
206
|
+
* Each entry names what stops holding when the row stops running. Membership
|
|
207
|
+
* needs one of two properties: disabling the row **fails open**, so the agent
|
|
208
|
+
* may afterwards do something it could not before, or it **removes evidence**
|
|
209
|
+
* without saying so, leaving nothing to reconstruct what the agent did. A row
|
|
210
|
+
* that fails closed on removal, or whose absence only takes a feature away, is
|
|
211
|
+
* not a member however security-adjacent its name reads.
|
|
212
|
+
*
|
|
213
|
+
* The rows examined against that rule and kept out are listed with their
|
|
214
|
+
* reasons under "What is deliberately not a finding" in `docs/checks.md`, so a
|
|
215
|
+
* reader can tell a considered exclusion from a row nobody looked at.
|
|
195
216
|
*/
|
|
196
217
|
export const SECURITY_ROW_IDS = new Map([
|
|
197
218
|
['approval', 'user approval prompts for tool calls'],
|
|
@@ -219,32 +240,138 @@ export const SECURITY_ROW_IDS = new Map([
|
|
|
219
240
|
* replaces a core service for every consumer in its scope.
|
|
220
241
|
*/
|
|
221
242
|
export const SEAM_KEYS = new Set([
|
|
222
|
-
'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', '
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'
|
|
243
|
+
'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'agentTeams', 'approval', 'attachments',
|
|
244
|
+
'authorization', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
|
|
245
|
+
'credentialsController', 'deepseekLlmApiExtensions', 'directoryPicker', 'directoryPickerController',
|
|
246
|
+
'e2b', 'fileReferences', 'fs', 'goals', 'inspector', 'invariants', 'jobs', 'llm', 'lsp',
|
|
247
|
+
'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy', 'sessionController',
|
|
248
|
+
'sessionFileReferences', 'sessionPersistence', 'sessionProjectionCache', 'sessionProjections',
|
|
249
|
+
'sessionQuery', 'sessionReferenceResolver', 'sessions', 'sessionSkillCatalog', 'sessionTelemetry',
|
|
250
|
+
'sessionTitle', 'settings', 'settingsController', 'shell', 'shellEnv', 'skills', 'spillStore', 'storage',
|
|
251
|
+
'storageDomain', 'subagentModelSelection', 'subagents', 'subprocess', 'systemPrompt', 'terminals',
|
|
252
|
+
'timer', 'tokenMeter', 'toolResultPruner', 'tools', 'typert', 'typertGateway', 'userQuestions', 'web',
|
|
253
|
+
'webhookRuntime', 'webServer', 'workflowEngine', 'workspaceController', 'workspaceRegistry',
|
|
232
254
|
]);
|
|
233
255
|
/**
|
|
234
|
-
* The subset of {@link SEAM_KEYS} whose replacement removes a constraint
|
|
256
|
+
* The subset of {@link SEAM_KEYS} whose replacement either removes a constraint
|
|
257
|
+
* or takes over the record by which one could be checked afterwards. Most
|
|
258
|
+
* members are the first kind — `approval`, `sandbox`, `credentials`. The set
|
|
259
|
+
* already holds two of the second, `sessionPersistence` and `sessionTelemetry`,
|
|
260
|
+
* and each entry below says which kind it is rather than borrowing the other's
|
|
261
|
+
* sentence.
|
|
262
|
+
*
|
|
263
|
+
* `authorization` is the registry of flows that obtain a credential through a
|
|
264
|
+
* conversation with the user, so providing it means owning that conversation.
|
|
265
|
+
* That is the same class of substitution as `credentials`, which this set
|
|
266
|
+
* already holds. `fileReferences` decides which paths are offered for
|
|
267
|
+
* completion and `agentTeams` is the team form of `subagents`, which is
|
|
268
|
+
* deliberately not here either, so neither of those is in this set.
|
|
269
|
+
*
|
|
270
|
+
* Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
|
|
271
|
+
* services that own one `ctx.remote.*` namespace the browser client calls
|
|
272
|
+
* across the wire. A controller belongs here only when the traffic a
|
|
273
|
+
* substitution redirects to it carries a secret, an execution boundary, or a
|
|
274
|
+
* decision. A controller that forwards its seam's own verbs and adds a wire
|
|
275
|
+
* failure vocabulary does not, because the seam it fronts is reachable from
|
|
276
|
+
* `ctx` without substituting anything.
|
|
277
|
+
*
|
|
278
|
+
* Included:
|
|
279
|
+
* - `credentialsController` is what a browser configuration page calls to store
|
|
280
|
+
* a credential. `set(ref, value)` receives the plaintext secret and hands it
|
|
281
|
+
* to `ctx.credentials`
|
|
282
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
|
|
283
|
+
* `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
|
|
284
|
+
* answer to the three fields `CredentialInfo` declares. A layer that provides
|
|
285
|
+
* it takes both halves: every secret typed into the settings page, and the
|
|
286
|
+
* freedom to answer a read with the stored value.
|
|
287
|
+
* - `settingsController` passes `redactSecrets: true` on every remote read
|
|
288
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
|
|
289
|
+
* which is what keeps a `role('secret')` field out of a settings response;
|
|
290
|
+
* `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
|
|
291
|
+
* `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
|
|
292
|
+
* value in plaintext from the configuration page. Providing it puts the layer
|
|
293
|
+
* on both directions of a secret's path.
|
|
294
|
+
* - `sessionController` resolves each new Session's cwd from the wire request
|
|
295
|
+
* and hands it to `ensureSession`
|
|
296
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
|
|
297
|
+
* `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
|
|
298
|
+
* `workspace-write` root the enforcing filesystem, bash and terminal backends
|
|
299
|
+
* fence against. Its `prompt` verb builds the message admitted to the agent
|
|
300
|
+
* under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
|
|
301
|
+
* sandbox root for every session created from the client, and the text that
|
|
302
|
+
* reaches the model under the user's own source label.
|
|
303
|
+
* - `webhookRuntime` is what a provider adapter such as
|
|
304
|
+
* `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
|
|
305
|
+
* one built-in action creates a Session from a rule result whose fields
|
|
306
|
+
* include `workspacePath`, `permissionPreset` and `prompt`
|
|
307
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
|
|
308
|
+
* `createWebhookSession` applies that preset through
|
|
309
|
+
* `ctx.permissionPresets.set` before admitting the prompt
|
|
310
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
|
|
311
|
+
* Providing it picks the approval and sandbox preset for an agent started by
|
|
312
|
+
* a remote delivery with no user present.
|
|
313
|
+
* - `inspector` is the second kind, and the only member graded from the
|
|
314
|
+
* catalogue rather than from an implementation. The catalogue declares it as
|
|
315
|
+
* the façade over the realm's source publisher, with `publish(topic, payload,
|
|
316
|
+
* monotonicMs?)` and a read-only `CordisRuntimeTreeReader`
|
|
317
|
+
* (`@deepseek-ai/dsh-tool-cordis/lib/index.js:1558`, the two method
|
|
318
|
+
* signatures at `:1562` and `:1579`). Providing it
|
|
319
|
+
* takes no decision away from anyone: nothing is gated on an observation, and
|
|
320
|
+
* on that ground the key does not belong beside `approval`. What it takes is
|
|
321
|
+
* the position observations pass through. A substituted publisher chooses
|
|
322
|
+
* which topics reach the carrier and what payload each one carries, so it can
|
|
323
|
+
* withhold the record of something that happened or publish one for something
|
|
324
|
+
* that did not, and a consumer downstream cannot tell either from a quiet
|
|
325
|
+
* system. That is the property this set already recognises in
|
|
326
|
+
* `sessionPersistence` and `sessionTelemetry`.
|
|
327
|
+
*
|
|
328
|
+
* No implementation exists to displace. In `0.1.2-rc.1` the key is declared
|
|
329
|
+
* once and used nowhere: `'inspector'` as a string literal occurs exactly
|
|
330
|
+
* once across the 224 `@deepseek-ai` packages in the installed tree — that
|
|
331
|
+
* catalogue entry — no file reads `ctx.inspector`, and `InspectorJsonValue`
|
|
332
|
+
* and `CordisRuntimeTreeReader` appear only in that same bundle. So a package
|
|
333
|
+
* providing `inspector` in this release displaces nothing and reaches nothing
|
|
334
|
+
* it could not reach under a name of its own. This entry grades what the
|
|
335
|
+
* catalogue says the key is for, not code that runs today, and it is the one
|
|
336
|
+
* entry a release that ships a publisher or a consumer should settle again
|
|
337
|
+
* against them.
|
|
235
338
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
339
|
+
* Excluded:
|
|
340
|
+
* - `workspaceController` forwards `request.path` to
|
|
341
|
+
* `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
|
|
342
|
+
* error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
|
|
343
|
+
* to `:212`). The registry it fronts is `workspaceRegistry`, which is not
|
|
344
|
+
* here, so the substitution reaches nothing the seam does not already offer.
|
|
345
|
+
* - `directoryPickerController` is three delegations to
|
|
346
|
+
* `ctx.directoryPicker.capability()` behind a check that refuses a verb the
|
|
347
|
+
* composed backend does not serve
|
|
348
|
+
* (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
|
|
349
|
+
* Path fencing lives in the backend, and `directoryPicker` is not here.
|
|
350
|
+
* - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
|
|
351
|
+
* is excluded above for the same reason: the traffic is path candidates
|
|
352
|
+
* offered for completion.
|
|
353
|
+
* - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
|
|
354
|
+
* for one Session's human-facing composer
|
|
355
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
|
|
356
|
+
* `:227`), and its only consumer in this release is the client skill picker
|
|
357
|
+
* (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
|
|
358
|
+
* model through `ctx.skills.list()` in
|
|
359
|
+
* `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
|
|
360
|
+
* - `subagentModelSelection` is a settings owner answering `{ enabled,
|
|
361
|
+
* allowedModels }`, sampled when an Agent receives its delegation tools. Model
|
|
362
|
+
* routing is `llm` and delegation is `subagents`; neither is here.
|
|
363
|
+
* - `deepseekLlmApiExtensions` hands a substitute the serialized request body
|
|
364
|
+
* and merges the fields it returns, but
|
|
365
|
+
* `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
|
|
366
|
+
* field colliding with the base request, so `messages`, `tools` and `model`
|
|
367
|
+
* are not writable through it. The constraint is in the adapter, not the
|
|
368
|
+
* registry the substitution replaces.
|
|
243
369
|
*/
|
|
244
370
|
export const SECURITY_SEAM_KEYS = new Set([
|
|
245
371
|
'approval', 'authorization', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials',
|
|
246
|
-
'
|
|
247
|
-
'
|
|
372
|
+
'credentialsController', 'settingsController', 'sessionController', 'webhookRuntime',
|
|
373
|
+
'subprocess', 'shell', 'fs', 'tools', 'agentLoop', 'inspector', 'sessionPersistence',
|
|
374
|
+
'sessionTelemetry', 'invariants',
|
|
248
375
|
]);
|
|
249
376
|
/**
|
|
250
377
|
* Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
|
|
@@ -253,19 +380,31 @@ export const SECURITY_SEAM_KEYS = new Set([
|
|
|
253
380
|
*
|
|
254
381
|
* Note there is no `fs/read-intent` — the intent family is write and edit only.
|
|
255
382
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
383
|
+
* `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
|
|
384
|
+
* and adds `user-questions/request`. Both replace content in a durable log copy
|
|
385
|
+
* or answer a pending request; neither is an `emit` event, so both hand a
|
|
386
|
+
* listener the trailing `next`.
|
|
259
387
|
*/
|
|
260
388
|
export const WATERFALL_EVENTS = new Set([
|
|
261
389
|
'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
|
|
262
390
|
'fs/edit-intent', 'fs/write-intent', 'llm/stream', 'session-telemetry/record',
|
|
263
|
-
'system-prompt/assemble', 'tools/
|
|
264
|
-
'tools/
|
|
391
|
+
'system-prompt/assemble', 'tools/execute', 'tools/post-execute',
|
|
392
|
+
'tools/pre-execute', 'tools/ptc-dispatch-log', 'user-questions/request',
|
|
265
393
|
]);
|
|
266
|
-
/**
|
|
394
|
+
/**
|
|
395
|
+
* Waterfall events whose short-circuit removes a decision the user would
|
|
396
|
+
* otherwise make.
|
|
397
|
+
*
|
|
398
|
+
* `user-questions/request` is here for the same reason `approval/request` is:
|
|
399
|
+
* `ctx.userQuestions` pauses a tool call until a human answers, and the
|
|
400
|
+
* answerers that put the question on a screen are listeners in the chain rather
|
|
401
|
+
* than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
|
|
402
|
+
* A listener that returns an answer without calling `next()` answers on the
|
|
403
|
+
* user's behalf and the question is never shown.
|
|
404
|
+
*/
|
|
267
405
|
export const DECISION_EVENTS = new Set([
|
|
268
406
|
'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
|
|
407
|
+
'user-questions/request',
|
|
269
408
|
]);
|
|
270
409
|
/**
|
|
271
410
|
* Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
|
|
@@ -418,3 +557,130 @@ export const HARNESS_INERT_CALLS = new Set([
|
|
|
418
557
|
* in code, and it is plain YAML.
|
|
419
558
|
*/
|
|
420
559
|
export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
|
|
560
|
+
/**
|
|
561
|
+
* How a Cordis waterfall listener delegates, and what happens when it does not.
|
|
562
|
+
*
|
|
563
|
+
* Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
|
|
564
|
+
* `lib/index.js:317-327`:
|
|
565
|
+
*
|
|
566
|
+
* ```js
|
|
567
|
+
* waterfall(...args) {
|
|
568
|
+
* const cbs = this.dispatch("waterfall", args);
|
|
569
|
+
* const inner = args.pop();
|
|
570
|
+
* const next = () => { return (cbs.shift() ?? inner)(...args); };
|
|
571
|
+
* args.push(next);
|
|
572
|
+
* return next();
|
|
573
|
+
* }
|
|
574
|
+
* ```
|
|
575
|
+
*
|
|
576
|
+
* `next` is the trailing argument every listener receives, and `inner` is the
|
|
577
|
+
* harness's own built-in behavior. A listener that returns without calling
|
|
578
|
+
* `next()` therefore ends the chain: neither the listeners still in `cbs` nor
|
|
579
|
+
* `inner` run.
|
|
580
|
+
*
|
|
581
|
+
* The scope of that is one dispatch, not the registry. `dispatch()` builds
|
|
582
|
+
* `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
|
|
583
|
+
* never touched and every skipped listener is registered and runs normally on
|
|
584
|
+
* the next dispatch. The precise word is veto, not removal — Cordis's own
|
|
585
|
+
* JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
|
|
586
|
+
* the built-in behavior". Removal is a different capability with a different
|
|
587
|
+
* reach, and it has its own table below.
|
|
588
|
+
*/
|
|
589
|
+
export const WATERFALL_NEXT_PARAMETER = 'next';
|
|
590
|
+
/**
|
|
591
|
+
* What each decision waterfall's built-in `next` settles on when no listener
|
|
592
|
+
* claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
|
|
593
|
+
*
|
|
594
|
+
* This is what a listener that never calls `next()` replaces. The inner
|
|
595
|
+
* callback is the last argument at each site:
|
|
596
|
+
* - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
|
|
597
|
+
* `() => Promise.resolve({ kind: "allow" })`
|
|
598
|
+
* - `tools/execute` — `dsh-tools/lib/index.js:3214`,
|
|
599
|
+
* `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
|
|
600
|
+
* body of the tool call itself
|
|
601
|
+
* - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
|
|
602
|
+
* `() => Promise.resolve("unavailable")`, and the surface that would ask the
|
|
603
|
+
* user is one of the listeners in the chain rather than the inner callback
|
|
604
|
+
* - `user-questions/request` —
|
|
605
|
+
* `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
|
|
606
|
+
* callback passed at `:69`, which rejects with a `UserQuestionError` carrying
|
|
607
|
+
* code `NO_PROVIDER`
|
|
608
|
+
*
|
|
609
|
+
* The three tables in this module that name events (`WATERFALL_EVENTS`,
|
|
610
|
+
* `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
|
|
611
|
+
*/
|
|
612
|
+
export const DECISION_EVENT_DEFAULTS = new Map([
|
|
613
|
+
['approval/request', 'the request falls through to `"unavailable"` only after every composed answerer — '
|
|
614
|
+
+ 'including the surface that would ask the user — has had the dispatch'],
|
|
615
|
+
['tools/pre-execute', 'the gate settles on `{ kind: "allow" }` after every other listener, and only then are '
|
|
616
|
+
+ '`ctx.tools.guard()` denials consulted'],
|
|
617
|
+
['tools/execute', 'the tool body itself runs'],
|
|
618
|
+
['fs/write-intent', 'the write intent reaches the policy rows that decide it'],
|
|
619
|
+
['fs/edit-intent', 'the edit intent reaches the policy rows that decide it'],
|
|
620
|
+
['user-questions/request', 'the request rejects with `NO_PROVIDER` only after every composed answerer — '
|
|
621
|
+
+ 'including the one that puts the question on the user\'s screen — has had the dispatch'],
|
|
622
|
+
]);
|
|
623
|
+
/**
|
|
624
|
+
* Receivers whose members name a plugin context.
|
|
625
|
+
*
|
|
626
|
+
* The same set the Tier C detached-member check guards on, minus `process`:
|
|
627
|
+
* a seam is read off the context, never off `process`.
|
|
628
|
+
*/
|
|
629
|
+
export const CONTEXT_RECEIVERS = new Set([
|
|
630
|
+
'ctx', 'context', 'globalThis', 'global',
|
|
631
|
+
]);
|
|
632
|
+
/**
|
|
633
|
+
* Array and collection methods that change the receiver rather than reading it.
|
|
634
|
+
*
|
|
635
|
+
* Used to tell a write into a service's internals from a read of them. The
|
|
636
|
+
* distinction is not academic: `dsh-dlp` reads
|
|
637
|
+
* `ctx.events._hooks['approval/request']?.length` to decide whether an ask
|
|
638
|
+
* would reach a human, which is an honest use of the same property a hostile
|
|
639
|
+
* layer splices.
|
|
640
|
+
*/
|
|
641
|
+
export const MUTATING_METHODS = new Set([
|
|
642
|
+
'splice', 'push', 'pop', 'shift', 'unshift', 'fill', 'sort', 'reverse', 'copyWithin',
|
|
643
|
+
'clear', 'delete', 'set', 'add',
|
|
644
|
+
]);
|
|
645
|
+
/**
|
|
646
|
+
* Cordis internals through which one plugin removes another plugin's
|
|
647
|
+
* registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
|
|
648
|
+
*
|
|
649
|
+
* None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
|
|
650
|
+
* `ctx.reflect` are own properties of the root context inherited by every
|
|
651
|
+
* child, so no `inject` declaration is needed to reach any of them.
|
|
652
|
+
*/
|
|
653
|
+
export const TEARDOWN_SURFACES = [
|
|
654
|
+
{
|
|
655
|
+
service: 'events',
|
|
656
|
+
member: '_hooks',
|
|
657
|
+
readIsEnough: false,
|
|
658
|
+
effect: 'the listener table every layer\'s `ctx.on()` registration is stored in (`lib/index.js:230`, '
|
|
659
|
+
+ '`_hooks = {}`, appended to by `register` at `lib/index.js:336-345`). Splicing an entry out removes that '
|
|
660
|
+
+ 'listener permanently, and the owning layer\'s own disposer then silently does nothing. This is a stronger '
|
|
661
|
+
+ 'reach than a waterfall veto, which only skips listeners for one dispatch',
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
service: 'events',
|
|
665
|
+
member: 'unregister',
|
|
666
|
+
readIsEnough: true,
|
|
667
|
+
effect: 'the public removal path for one listener, by callback identity (`lib/index.js:353-359`). It takes the '
|
|
668
|
+
+ 'listener list and a callback and splices, with no check that the caller owns either',
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
service: 'registry',
|
|
672
|
+
member: 'delete',
|
|
673
|
+
readIsEnough: true,
|
|
674
|
+
effect: 'disposal of every fiber a plugin owns (`lib/index.js:1564-1571`: `for (const fiber of runtime.fibers) '
|
|
675
|
+
+ 'fiber.dispose();`). It takes no ownership check, so one layer can unload another layer outright — '
|
|
676
|
+
+ 'including a security layer whose guards and listeners then stop existing',
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
service: 'reflect',
|
|
680
|
+
member: 'store',
|
|
681
|
+
readIsEnough: false,
|
|
682
|
+
effect: 'the service implementation table keyed by isolate symbol (`lib/index.js:726`, written by `provide` at '
|
|
683
|
+
+ '`lib/index.js:813`). `provide` throws when a key is already taken and `set` throws across fibers; writing '
|
|
684
|
+
+ 'this object directly is the path around both throws',
|
|
685
|
+
},
|
|
686
|
+
];
|
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
|
/**
|
|
@@ -58,7 +60,16 @@ export declare const CORE_ROW_IDS: ReadonlySet<string>;
|
|
|
58
60
|
* Disabling or reconfiguring one of these from a third-party patch layer is
|
|
59
61
|
* the highest-value finding this tool produces, and it is plain YAML.
|
|
60
62
|
*
|
|
61
|
-
* Each entry names what stops holding when the row stops running.
|
|
63
|
+
* Each entry names what stops holding when the row stops running. Membership
|
|
64
|
+
* needs one of two properties: disabling the row **fails open**, so the agent
|
|
65
|
+
* may afterwards do something it could not before, or it **removes evidence**
|
|
66
|
+
* without saying so, leaving nothing to reconstruct what the agent did. A row
|
|
67
|
+
* that fails closed on removal, or whose absence only takes a feature away, is
|
|
68
|
+
* not a member however security-adjacent its name reads.
|
|
69
|
+
*
|
|
70
|
+
* The rows examined against that rule and kept out are listed with their
|
|
71
|
+
* reasons under "What is deliberately not a finding" in `docs/checks.md`, so a
|
|
72
|
+
* reader can tell a considered exclusion from a row nobody looked at.
|
|
62
73
|
*/
|
|
63
74
|
export declare const SECURITY_ROW_IDS: ReadonlyMap<string, string>;
|
|
64
75
|
/**
|
|
@@ -69,15 +80,119 @@ export declare const SECURITY_ROW_IDS: ReadonlyMap<string, string>;
|
|
|
69
80
|
*/
|
|
70
81
|
export declare const SEAM_KEYS: ReadonlySet<string>;
|
|
71
82
|
/**
|
|
72
|
-
* The subset of {@link SEAM_KEYS} whose replacement removes a constraint
|
|
83
|
+
* The subset of {@link SEAM_KEYS} whose replacement either removes a constraint
|
|
84
|
+
* or takes over the record by which one could be checked afterwards. Most
|
|
85
|
+
* members are the first kind — `approval`, `sandbox`, `credentials`. The set
|
|
86
|
+
* already holds two of the second, `sessionPersistence` and `sessionTelemetry`,
|
|
87
|
+
* and each entry below says which kind it is rather than borrowing the other's
|
|
88
|
+
* sentence.
|
|
89
|
+
*
|
|
90
|
+
* `authorization` is the registry of flows that obtain a credential through a
|
|
91
|
+
* conversation with the user, so providing it means owning that conversation.
|
|
92
|
+
* That is the same class of substitution as `credentials`, which this set
|
|
93
|
+
* already holds. `fileReferences` decides which paths are offered for
|
|
94
|
+
* completion and `agentTeams` is the team form of `subagents`, which is
|
|
95
|
+
* deliberately not here either, so neither of those is in this set.
|
|
96
|
+
*
|
|
97
|
+
* Seven of the eleven keys `0.1.2-rc.1` adds are Remote controllers: host
|
|
98
|
+
* services that own one `ctx.remote.*` namespace the browser client calls
|
|
99
|
+
* across the wire. A controller belongs here only when the traffic a
|
|
100
|
+
* substitution redirects to it carries a secret, an execution boundary, or a
|
|
101
|
+
* decision. A controller that forwards its seam's own verbs and adds a wire
|
|
102
|
+
* failure vocabulary does not, because the seam it fronts is reachable from
|
|
103
|
+
* `ctx` without substituting anything.
|
|
104
|
+
*
|
|
105
|
+
* Included:
|
|
106
|
+
* - `credentialsController` is what a browser configuration page calls to store
|
|
107
|
+
* a credential. `set(ref, value)` receives the plaintext secret and hands it
|
|
108
|
+
* to `ctx.credentials`
|
|
109
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:171`), and
|
|
110
|
+
* `projectCredentialInfo` at `lib/index.js:78` is what holds a `describe`
|
|
111
|
+
* answer to the three fields `CredentialInfo` declares. A layer that provides
|
|
112
|
+
* it takes both halves: every secret typed into the settings page, and the
|
|
113
|
+
* freedom to answer a read with the stored value.
|
|
114
|
+
* - `settingsController` passes `redactSecrets: true` on every remote read
|
|
115
|
+
* (`@deepseek-ai/dsh-api-settings-controller/lib/index.js:429` and `:544`),
|
|
116
|
+
* which is what keeps a `role('secret')` field out of a settings response;
|
|
117
|
+
* `@deepseek-ai/dsh-web-search-deepseek/lib/index.js:245` declares one, an
|
|
118
|
+
* `apiKey`. Its `update`, `replace` and `mutate` verbs carry that same field's
|
|
119
|
+
* value in plaintext from the configuration page. Providing it puts the layer
|
|
120
|
+
* on both directions of a secret's path.
|
|
121
|
+
* - `sessionController` resolves each new Session's cwd from the wire request
|
|
122
|
+
* and hands it to `ensureSession`
|
|
123
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/index.js:574`), and
|
|
124
|
+
* `@deepseek-ai/dsh-sandbox-policy` resolves that immutable cwd as the
|
|
125
|
+
* `workspace-write` root the enforcing filesystem, bash and terminal backends
|
|
126
|
+
* fence against. Its `prompt` verb builds the message admitted to the agent
|
|
127
|
+
* under `source.kind: 'user'` (`lib/index.js:731`). Providing it chooses the
|
|
128
|
+
* sandbox root for every session created from the client, and the text that
|
|
129
|
+
* reaches the model under the user's own source label.
|
|
130
|
+
* - `webhookRuntime` is what a provider adapter such as
|
|
131
|
+
* `@deepseek-ai/dsh-webhook-github` dispatches verified deliveries into. Its
|
|
132
|
+
* one built-in action creates a Session from a rule result whose fields
|
|
133
|
+
* include `workspacePath`, `permissionPreset` and `prompt`
|
|
134
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/types.d.ts`), and
|
|
135
|
+
* `createWebhookSession` applies that preset through
|
|
136
|
+
* `ctx.permissionPresets.set` before admitting the prompt
|
|
137
|
+
* (`@deepseek-ai/dsh-webhook/lib/types/session.js:94`, `:117`, `:120`).
|
|
138
|
+
* Providing it picks the approval and sandbox preset for an agent started by
|
|
139
|
+
* a remote delivery with no user present.
|
|
140
|
+
* - `inspector` is the second kind, and the only member graded from the
|
|
141
|
+
* catalogue rather than from an implementation. The catalogue declares it as
|
|
142
|
+
* the façade over the realm's source publisher, with `publish(topic, payload,
|
|
143
|
+
* monotonicMs?)` and a read-only `CordisRuntimeTreeReader`
|
|
144
|
+
* (`@deepseek-ai/dsh-tool-cordis/lib/index.js:1558`, the two method
|
|
145
|
+
* signatures at `:1562` and `:1579`). Providing it
|
|
146
|
+
* takes no decision away from anyone: nothing is gated on an observation, and
|
|
147
|
+
* on that ground the key does not belong beside `approval`. What it takes is
|
|
148
|
+
* the position observations pass through. A substituted publisher chooses
|
|
149
|
+
* which topics reach the carrier and what payload each one carries, so it can
|
|
150
|
+
* withhold the record of something that happened or publish one for something
|
|
151
|
+
* that did not, and a consumer downstream cannot tell either from a quiet
|
|
152
|
+
* system. That is the property this set already recognises in
|
|
153
|
+
* `sessionPersistence` and `sessionTelemetry`.
|
|
73
154
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
155
|
+
* No implementation exists to displace. In `0.1.2-rc.1` the key is declared
|
|
156
|
+
* once and used nowhere: `'inspector'` as a string literal occurs exactly
|
|
157
|
+
* once across the 224 `@deepseek-ai` packages in the installed tree — that
|
|
158
|
+
* catalogue entry — no file reads `ctx.inspector`, and `InspectorJsonValue`
|
|
159
|
+
* and `CordisRuntimeTreeReader` appear only in that same bundle. So a package
|
|
160
|
+
* providing `inspector` in this release displaces nothing and reaches nothing
|
|
161
|
+
* it could not reach under a name of its own. This entry grades what the
|
|
162
|
+
* catalogue says the key is for, not code that runs today, and it is the one
|
|
163
|
+
* entry a release that ships a publisher or a consumer should settle again
|
|
164
|
+
* against them.
|
|
165
|
+
*
|
|
166
|
+
* Excluded:
|
|
167
|
+
* - `workspaceController` forwards `request.path` to
|
|
168
|
+
* `ctx.workspaceRegistry.create` unchanged and adds an ordering queue and
|
|
169
|
+
* error mapping (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:196`
|
|
170
|
+
* to `:212`). The registry it fronts is `workspaceRegistry`, which is not
|
|
171
|
+
* here, so the substitution reaches nothing the seam does not already offer.
|
|
172
|
+
* - `directoryPickerController` is three delegations to
|
|
173
|
+
* `ctx.directoryPicker.capability()` behind a check that refuses a verb the
|
|
174
|
+
* composed backend does not serve
|
|
175
|
+
* (`@deepseek-ai/dsh-api-workspace-controller/lib/index.js:423` to `:470`).
|
|
176
|
+
* Path fencing lives in the backend, and `directoryPicker` is not here.
|
|
177
|
+
* - `sessionFileReferences` is the Remote adapter over `fileReferences`, which
|
|
178
|
+
* is excluded above for the same reason: the traffic is path candidates
|
|
179
|
+
* offered for completion.
|
|
180
|
+
* - `sessionSkillCatalog` answers with `SkillListValue`, declared as the list
|
|
181
|
+
* for one Session's human-facing composer
|
|
182
|
+
* (`@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts:213` to
|
|
183
|
+
* `:227`), and its only consumer in this release is the client skill picker
|
|
184
|
+
* (`@deepseek-ai/dsh-client-ui-skill/lib/client.js:236`). Skill text reaches a
|
|
185
|
+
* model through `ctx.skills.list()` in
|
|
186
|
+
* `@deepseek-ai/dsh-tool-skill/lib/index.js:145`, which is the `skills` seam.
|
|
187
|
+
* - `subagentModelSelection` is a settings owner answering `{ enabled,
|
|
188
|
+
* allowedModels }`, sampled when an Agent receives its delegation tools. Model
|
|
189
|
+
* routing is `llm` and delegation is `subagents`; neither is here.
|
|
190
|
+
* - `deepseekLlmApiExtensions` hands a substitute the serialized request body
|
|
191
|
+
* and merges the fields it returns, but
|
|
192
|
+
* `@deepseek-ai/dsh-llm-deepseek/lib/index.js:1748` rejects any extension
|
|
193
|
+
* field colliding with the base request, so `messages`, `tools` and `model`
|
|
194
|
+
* are not writable through it. The constraint is in the adapter, not the
|
|
195
|
+
* registry the substitution replaces.
|
|
81
196
|
*/
|
|
82
197
|
export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
|
|
83
198
|
/**
|
|
@@ -87,12 +202,23 @@ export declare const SECURITY_SEAM_KEYS: ReadonlySet<string>;
|
|
|
87
202
|
*
|
|
88
203
|
* Note there is no `fs/read-intent` — the intent family is write and edit only.
|
|
89
204
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
205
|
+
* `0.1.2-rc.1` renames `tools/code-dispatch-log` to `tools/ptc-dispatch-log`
|
|
206
|
+
* and adds `user-questions/request`. Both replace content in a durable log copy
|
|
207
|
+
* or answer a pending request; neither is an `emit` event, so both hand a
|
|
208
|
+
* listener the trailing `next`.
|
|
93
209
|
*/
|
|
94
210
|
export declare const WATERFALL_EVENTS: ReadonlySet<string>;
|
|
95
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Waterfall events whose short-circuit removes a decision the user would
|
|
213
|
+
* otherwise make.
|
|
214
|
+
*
|
|
215
|
+
* `user-questions/request` is here for the same reason `approval/request` is:
|
|
216
|
+
* `ctx.userQuestions` pauses a tool call until a human answers, and the
|
|
217
|
+
* answerers that put the question on a screen are listeners in the chain rather
|
|
218
|
+
* than the inner callback (`@deepseek-ai/dsh-user-questions/lib/index.js:69`).
|
|
219
|
+
* A listener that returns an answer without calling `next()` answers on the
|
|
220
|
+
* user's behalf and the question is never shown.
|
|
221
|
+
*/
|
|
96
222
|
export declare const DECISION_EVENTS: ReadonlySet<string>;
|
|
97
223
|
/**
|
|
98
224
|
* Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
|
|
@@ -208,4 +334,97 @@ export declare const HARNESS_INERT_CALLS: ReadonlySet<string>;
|
|
|
208
334
|
* in code, and it is plain YAML.
|
|
209
335
|
*/
|
|
210
336
|
export declare const SERVICE_REMAPPING_FIELDS: readonly string[];
|
|
337
|
+
/**
|
|
338
|
+
* How a Cordis waterfall listener delegates, and what happens when it does not.
|
|
339
|
+
*
|
|
340
|
+
* Read out of the installed `@deepseek-ai/cordis@4.0.2` build,
|
|
341
|
+
* `lib/index.js:317-327`:
|
|
342
|
+
*
|
|
343
|
+
* ```js
|
|
344
|
+
* waterfall(...args) {
|
|
345
|
+
* const cbs = this.dispatch("waterfall", args);
|
|
346
|
+
* const inner = args.pop();
|
|
347
|
+
* const next = () => { return (cbs.shift() ?? inner)(...args); };
|
|
348
|
+
* args.push(next);
|
|
349
|
+
* return next();
|
|
350
|
+
* }
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* `next` is the trailing argument every listener receives, and `inner` is the
|
|
354
|
+
* harness's own built-in behavior. A listener that returns without calling
|
|
355
|
+
* `next()` therefore ends the chain: neither the listeners still in `cbs` nor
|
|
356
|
+
* `inner` run.
|
|
357
|
+
*
|
|
358
|
+
* The scope of that is one dispatch, not the registry. `dispatch()` builds
|
|
359
|
+
* `cbs` with `.filter(…).map(…)`, which allocates, so `this._hooks[name]` is
|
|
360
|
+
* never touched and every skipped listener is registered and runs normally on
|
|
361
|
+
* the next dispatch. The precise word is veto, not removal — Cordis's own
|
|
362
|
+
* JSDoc at `lib/index.js:311-313` says "vetoes the rest of the chain, including
|
|
363
|
+
* the built-in behavior". Removal is a different capability with a different
|
|
364
|
+
* reach, and it has its own table below.
|
|
365
|
+
*/
|
|
366
|
+
export declare const WATERFALL_NEXT_PARAMETER = "next";
|
|
367
|
+
/**
|
|
368
|
+
* What each decision waterfall's built-in `next` settles on when no listener
|
|
369
|
+
* claims the dispatch, transcribed from the installed harness `0.1.2-rc.1`.
|
|
370
|
+
*
|
|
371
|
+
* This is what a listener that never calls `next()` replaces. The inner
|
|
372
|
+
* callback is the last argument at each site:
|
|
373
|
+
* - `tools/pre-execute` — `@deepseek-ai/dsh-tools/lib/index.js:3117`,
|
|
374
|
+
* `() => Promise.resolve({ kind: "allow" })`
|
|
375
|
+
* - `tools/execute` — `dsh-tools/lib/index.js:3214`,
|
|
376
|
+
* `() => this.dispatchToolBody(mutableExec)`, so vetoing it substitutes the
|
|
377
|
+
* body of the tool call itself
|
|
378
|
+
* - `approval/request` — `@deepseek-ai/dsh-user-approval/lib/index.js:179`,
|
|
379
|
+
* `() => Promise.resolve("unavailable")`, and the surface that would ask the
|
|
380
|
+
* user is one of the listeners in the chain rather than the inner callback
|
|
381
|
+
* - `user-questions/request` —
|
|
382
|
+
* `@deepseek-ai/dsh-user-questions/lib/index.js:67`, the `noAnswerer`
|
|
383
|
+
* callback passed at `:69`, which rejects with a `UserQuestionError` carrying
|
|
384
|
+
* code `NO_PROVIDER`
|
|
385
|
+
*
|
|
386
|
+
* The three tables in this module that name events (`WATERFALL_EVENTS`,
|
|
387
|
+
* `DECISION_EVENTS`, and this one) are keyed to {@link HARNESS_REFERENCE}.
|
|
388
|
+
*/
|
|
389
|
+
export declare const DECISION_EVENT_DEFAULTS: ReadonlyMap<string, string>;
|
|
390
|
+
/**
|
|
391
|
+
* Receivers whose members name a plugin context.
|
|
392
|
+
*
|
|
393
|
+
* The same set the Tier C detached-member check guards on, minus `process`:
|
|
394
|
+
* a seam is read off the context, never off `process`.
|
|
395
|
+
*/
|
|
396
|
+
export declare const CONTEXT_RECEIVERS: ReadonlySet<string>;
|
|
397
|
+
/**
|
|
398
|
+
* Array and collection methods that change the receiver rather than reading it.
|
|
399
|
+
*
|
|
400
|
+
* Used to tell a write into a service's internals from a read of them. The
|
|
401
|
+
* distinction is not academic: `dsh-dlp` reads
|
|
402
|
+
* `ctx.events._hooks['approval/request']?.length` to decide whether an ask
|
|
403
|
+
* would reach a human, which is an honest use of the same property a hostile
|
|
404
|
+
* layer splices.
|
|
405
|
+
*/
|
|
406
|
+
export declare const MUTATING_METHODS: ReadonlySet<string>;
|
|
407
|
+
/** One Cordis bookkeeping surface that owns other plugins' registrations. */
|
|
408
|
+
export interface TeardownSurface {
|
|
409
|
+
/** The member read off the context, e.g. `events`. */
|
|
410
|
+
readonly service: string;
|
|
411
|
+
/** The member read off that, e.g. `_hooks`. */
|
|
412
|
+
readonly member: string;
|
|
413
|
+
/**
|
|
414
|
+
* True when merely naming the surface is the finding. False when only a
|
|
415
|
+
* write counts, because reading it is something an honest plugin does.
|
|
416
|
+
*/
|
|
417
|
+
readonly readIsEnough: boolean;
|
|
418
|
+
/** What reaching it does, phrased for a report. */
|
|
419
|
+
readonly effect: string;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Cordis internals through which one plugin removes another plugin's
|
|
423
|
+
* registrations. Read from the installed `@deepseek-ai/cordis@4.0.2` build.
|
|
424
|
+
*
|
|
425
|
+
* None of these is guarded by ownership. `ctx.events`, `ctx.registry` and
|
|
426
|
+
* `ctx.reflect` are own properties of the root context inherited by every
|
|
427
|
+
* child, so no `inject` declaration is needed to reach any of them.
|
|
428
|
+
*/
|
|
429
|
+
export declare const TEARDOWN_SURFACES: readonly TeardownSurface[];
|
|
211
430
|
//# sourceMappingURL=knowledge.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-inspector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Know what a DeepSeek Harness plugin does before you install it — static pre-install analysis of a plugin directory or tarball",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivan Tyshchenko",
|