@stratusagent/plugins 0.6.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +105 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +158 -0
- package/dist/loader.js.map +1 -0
- package/dist/manifest.d.ts +110 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +264 -0
- package/dist/manifest.js.map +1 -0
- package/dist/registry.d.ts +64 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +111 -0
- package/dist/registry.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stratus Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @stratusagent/plugins
|
|
2
|
+
|
|
3
|
+
The plugin host: what a plugin claims, what it is allowed to register, and how
|
|
4
|
+
a config block becomes running capability.
|
|
5
|
+
|
|
6
|
+
The contract lives in
|
|
7
|
+
[`docs/architecture/plugins.md`](../../docs/architecture/plugins.md) and is not
|
|
8
|
+
re-derived here. This package is what makes it enforceable.
|
|
9
|
+
|
|
10
|
+
You do not install this directly — it comes with `@stratusagent/cli`. You do
|
|
11
|
+
read it if you are writing a plugin.
|
|
12
|
+
|
|
13
|
+
## What a plugin author needs
|
|
14
|
+
|
|
15
|
+
A `stratus` field in `package.json`, and one named export:
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
{
|
|
19
|
+
"name": "stratus-plugin-notes",
|
|
20
|
+
"keywords": ["stratus-plugin"],
|
|
21
|
+
"stratus": {
|
|
22
|
+
"pluginVersion": 1,
|
|
23
|
+
"contributes": {
|
|
24
|
+
"tools": [
|
|
25
|
+
{ "name": "notes.read", "risk": "safe" },
|
|
26
|
+
{ "name": "notes.write", "risk": "gated" }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"config": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": { "vault": { "type": "string" } },
|
|
32
|
+
"required": ["vault"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export const createPlugin = (config) => ({
|
|
40
|
+
name: 'stratus-plugin-notes',
|
|
41
|
+
setup(context) {
|
|
42
|
+
context.tools.register(readTool(config));
|
|
43
|
+
},
|
|
44
|
+
// Optional. Only if setup acquired something a daemon shutdown must release.
|
|
45
|
+
async dispose() {},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What the host does with it
|
|
50
|
+
|
|
51
|
+
- **Reads the manifest without importing the package.** A misconfigured or
|
|
52
|
+
over-reaching plugin fails at load, with its own name in the message, rather
|
|
53
|
+
than at the first tool call in the middle of somebody's turn.
|
|
54
|
+
- **Validates your config against your schema** — including each per-agent
|
|
55
|
+
entry under `agents`, with `required` relaxed there, because an override says
|
|
56
|
+
what differs for one agent rather than restating the defaults.
|
|
57
|
+
- **Hands `setup()` a manifest-bound registry, not the real one.** A name your
|
|
58
|
+
manifest does not declare is refused. The risk a tool registers at is the
|
|
59
|
+
riskiest of three claims — your manifest's, the floor your package is held
|
|
60
|
+
to, and whatever the object says about itself — so a tool can raise itself
|
|
61
|
+
and can never talk its way below what it declared.
|
|
62
|
+
- **Stages and commits.** Nothing reaches the shared registry until your whole
|
|
63
|
+
`setup()` has succeeded, so a plugin that over-reaches on its fourth tool
|
|
64
|
+
leaves none of the first three behind.
|
|
65
|
+
- **Refuses collisions.** Two packages contributing one tool name is a
|
|
66
|
+
load-time error naming both, never whichever loaded last.
|
|
67
|
+
|
|
68
|
+
## Resolving settings per call
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { resolvePluginAgentConfig } from '@stratusagent/plugins';
|
|
72
|
+
|
|
73
|
+
async execute(input, session) {
|
|
74
|
+
const settings = resolvePluginAgentConfig(config, session.agent.id);
|
|
75
|
+
…
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Per call, from the session — not once at setup. For a plugin whose settings
|
|
80
|
+
are an access boundary (`tool-fs` roots are the example), resolving once hands
|
|
81
|
+
every agent whichever value was resolved first, which is one agent reading
|
|
82
|
+
another's files from code that looks correct.
|
|
83
|
+
|
|
84
|
+
## `loadOptionalModule`
|
|
85
|
+
|
|
86
|
+
The resolve-then-import split, in one place:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const slack = await loadOptionalModule('@stratusagent/channel-slack', {
|
|
90
|
+
resolve: (id) => import.meta.resolve(id),
|
|
91
|
+
import: (id) => import(id),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Only a package failing to **resolve** means "not installed". One that is
|
|
96
|
+
installed but missing a dependency of its own throws `ERR_MODULE_NOT_FOUND`
|
|
97
|
+
too, naming that dependency — so reading the import's error would turn a broken
|
|
98
|
+
install into a silently disabled feature.
|
|
99
|
+
|
|
100
|
+
The two capabilities come from the caller because `import.meta.resolve` answers
|
|
101
|
+
relative to the module that calls it: resolvable *from the daemon* and
|
|
102
|
+
resolvable *from this package* are different questions, and only the first is
|
|
103
|
+
the one being asked.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin host: what a plugin claims, what it is allowed to register,
|
|
3
|
+
* and how a config block becomes running capability.
|
|
4
|
+
*
|
|
5
|
+
* The contract lives in `docs/architecture/plugins.md` and is not
|
|
6
|
+
* re-derived here. What this package adds is enforcement — a manifest read
|
|
7
|
+
* without importing the package it describes, a registration view that
|
|
8
|
+
* holds `setup()` to that manifest, and a loader that refuses rather than
|
|
9
|
+
* degrades when a plugin over-reaches.
|
|
10
|
+
*/
|
|
11
|
+
export { declaredRiskFor, parsePluginManifest, resolvePluginAgentConfig, validateAgainstSchema, validatePluginConfig, HOST_CONFIG_KEYS, PLUGIN_MANIFEST_VERSION, PluginConfigError, PluginManifestError, type PluginContributions, type PluginManifest, type PluginNamespaceDeclaration, type PluginSkillDeclaration, type PluginToolDeclaration, } from './manifest.ts';
|
|
12
|
+
export { ManifestBoundToolRegistry, riskFloorFor, type PluginToolRecord, } from './registry.ts';
|
|
13
|
+
export { isFirstPartyPackage, loadOptionalModule, loadPlugins, type CreatePlugin, type LoadedPlugin, type LoadPluginsOptions, type LoadPluginsResult, type OptionalModuleHost, type PluginLoadFailure, } from './loader.ts';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin host: what a plugin claims, what it is allowed to register,
|
|
3
|
+
* and how a config block becomes running capability.
|
|
4
|
+
*
|
|
5
|
+
* The contract lives in `docs/architecture/plugins.md` and is not
|
|
6
|
+
* re-derived here. What this package adds is enforcement — a manifest read
|
|
7
|
+
* without importing the package it describes, a registration view that
|
|
8
|
+
* holds `setup()` to that manifest, and a loader that refuses rather than
|
|
9
|
+
* degrades when a plugin over-reaches.
|
|
10
|
+
*/
|
|
11
|
+
export { declaredRiskFor, parsePluginManifest, resolvePluginAgentConfig, validateAgainstSchema, validatePluginConfig, HOST_CONFIG_KEYS, PLUGIN_MANIFEST_VERSION, PluginConfigError, PluginManifestError, } from "./manifest.js";
|
|
12
|
+
export { ManifestBoundToolRegistry, riskFloorFor, } from "./registry.js";
|
|
13
|
+
export { isFirstPartyPackage, loadOptionalModule, loadPlugins, } from "./loader.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,GAMpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,yBAAyB,EACzB,YAAY,GAEb,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,GAOZ,MAAM,aAAa,CAAC"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { EventBus, JsonObject, Plugin, ToolRegistry } from '@stratusagent/core';
|
|
2
|
+
import { type PluginManifest } from './manifest.ts';
|
|
3
|
+
import { type PluginToolRecord } from './registry.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The two capabilities loading an optional package needs, taken from the
|
|
6
|
+
* caller rather than used here.
|
|
7
|
+
*
|
|
8
|
+
* `import.meta.resolve` answers relative to the module that calls it, so a
|
|
9
|
+
* helper that called its own would answer for this package's node_modules
|
|
10
|
+
* rather than the host's — resolvable from the daemon and resolvable from
|
|
11
|
+
* here are different questions, and only the first one is the one being
|
|
12
|
+
* asked. Callers pass `{ resolve: (id) => import.meta.resolve(id), import:
|
|
13
|
+
* (id) => import(id) }` and get the rule without a copy of it.
|
|
14
|
+
*/
|
|
15
|
+
export interface OptionalModuleHost {
|
|
16
|
+
resolve(specifier: string): string;
|
|
17
|
+
import(specifier: string): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a package that may not be installed, or report that it is not.
|
|
21
|
+
*
|
|
22
|
+
* Resolution and loading are separate questions, and only the first one
|
|
23
|
+
* means "not installed". Inspecting the import's error cannot tell them
|
|
24
|
+
* apart: a package that IS installed but is missing one of its own
|
|
25
|
+
* dependencies throws `ERR_MODULE_NOT_FOUND` too, naming that dependency —
|
|
26
|
+
* so a broken install would read as an absent one, silently disabling
|
|
27
|
+
* something whose configuration says it should be running.
|
|
28
|
+
*
|
|
29
|
+
* Extracted because there were three copies of it — the Slack adapter, the
|
|
30
|
+
* control API, and the dashboard resolved from inside the control API —
|
|
31
|
+
* and a plugin loader would have been the fourth.
|
|
32
|
+
*/
|
|
33
|
+
export declare const loadOptionalModule: <T = unknown>(specifier: string, host: OptionalModuleHost) => Promise<T | undefined>;
|
|
34
|
+
/** The ABI every loadable plugin exports. See `plugins.md`. */
|
|
35
|
+
export type CreatePlugin = (config: JsonObject) => Plugin | Promise<Plugin>;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a package's code is trusted — which is to say whether its
|
|
38
|
+
* manifest may declare a tool `safe`.
|
|
39
|
+
*
|
|
40
|
+
* First-party packages ship from this repository and pass its CI; anything
|
|
41
|
+
* else floors at `gated`. An operator can widen the set deliberately, and
|
|
42
|
+
* doing so is the same act as enabling the plugin at all: enablement is the
|
|
43
|
+
* security boundary, and this is a statement about how far it extends.
|
|
44
|
+
*/
|
|
45
|
+
export declare const isFirstPartyPackage: (packageName: string) => boolean;
|
|
46
|
+
export interface LoadedPlugin {
|
|
47
|
+
/** The package name — a plugin's identity is its package. */
|
|
48
|
+
package: string;
|
|
49
|
+
/** What the plugin called itself in its `Plugin.name`. */
|
|
50
|
+
name: string;
|
|
51
|
+
manifest: PluginManifest;
|
|
52
|
+
trusted: boolean;
|
|
53
|
+
tools: PluginToolRecord[];
|
|
54
|
+
/**
|
|
55
|
+
* The plugin itself, so the host can shut it down. A browser plugin holds
|
|
56
|
+
* a Chromium and a listening socket; a daemon that stopped without
|
|
57
|
+
* telling it would leak both.
|
|
58
|
+
*/
|
|
59
|
+
instance: Plugin;
|
|
60
|
+
}
|
|
61
|
+
/** A plugin an operator asked for that did not load, and why. */
|
|
62
|
+
export interface PluginLoadFailure {
|
|
63
|
+
package: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
}
|
|
66
|
+
export interface LoadPluginsOptions {
|
|
67
|
+
/** The `plugins` block: package name to that package's settings. */
|
|
68
|
+
config: Record<string, JsonObject> | undefined;
|
|
69
|
+
host: OptionalModuleHost;
|
|
70
|
+
/** The registry a plugin's tools are committed into once it loads whole. */
|
|
71
|
+
tools: ToolRegistry;
|
|
72
|
+
bus: EventBus;
|
|
73
|
+
/**
|
|
74
|
+
* Where tool output belongs on this machine. Supplied to any plugin whose
|
|
75
|
+
* schema declares `workspaceRoot` and whose operator did not set one —
|
|
76
|
+
* the host knows the platform's answer (`~/.stratus/workspaces`) and a
|
|
77
|
+
* plugin that had to derive it would be re-deriving a path this
|
|
78
|
+
* repository owns.
|
|
79
|
+
*/
|
|
80
|
+
workspaceRoot?: string;
|
|
81
|
+
/** Overrides the trusted set. See `isFirstPartyPackage`. */
|
|
82
|
+
trusted?: (packageName: string) => boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface LoadPluginsResult {
|
|
85
|
+
loaded: LoadedPlugin[];
|
|
86
|
+
failures: PluginLoadFailure[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Turn a `plugins` config block into running capability.
|
|
90
|
+
*
|
|
91
|
+
* Ordered the way the trust model requires: **nothing auto-loads** (only
|
|
92
|
+
* what is listed and enabled, and callers pass this only what a trusted
|
|
93
|
+
* config named), the **manifest is validated before the module is
|
|
94
|
+
* imported**, and `setup()` registers through the manifest-bound view
|
|
95
|
+
* rather than the raw registry.
|
|
96
|
+
*
|
|
97
|
+
* One plugin failing does not take the others — or the daemon — down: a
|
|
98
|
+
* failure is collected, named, and reported to the operator, because a
|
|
99
|
+
* daemon that refuses to start over a mistyped package name is a worse
|
|
100
|
+
* outcome than one running the plugins that do load and saying which did
|
|
101
|
+
* not. What is *never* degraded is the security half: an undeclared name, a
|
|
102
|
+
* collision, or a manifest that will not parse refuses that plugin whole.
|
|
103
|
+
*/
|
|
104
|
+
export declare const loadPlugins: (options: LoadPluginsOptions) => Promise<LoadPluginsResult>;
|
|
105
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAErF,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7C;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAAU,CAAC,GAAG,OAAO,EAClD,WAAW,MAAM,EACjB,MAAM,kBAAkB,KACvB,OAAO,CAAC,CAAC,GAAG,SAAS,CASvB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAuB5E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,aAAa,MAAM,KAAG,OACmB,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC;IAC/C,IAAI,EAAE,kBAAkB,CAAC;IACzB,4EAA4E;IAC5E,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,QAAQ,CAAC;IACd;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAuBD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,iBAAiB,CA0ExF,CAAC"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parsePluginManifest, validatePluginConfig, PluginManifestError, } from "./manifest.js";
|
|
5
|
+
import { ManifestBoundToolRegistry } from "./registry.js";
|
|
6
|
+
/**
|
|
7
|
+
* Load a package that may not be installed, or report that it is not.
|
|
8
|
+
*
|
|
9
|
+
* Resolution and loading are separate questions, and only the first one
|
|
10
|
+
* means "not installed". Inspecting the import's error cannot tell them
|
|
11
|
+
* apart: a package that IS installed but is missing one of its own
|
|
12
|
+
* dependencies throws `ERR_MODULE_NOT_FOUND` too, naming that dependency —
|
|
13
|
+
* so a broken install would read as an absent one, silently disabling
|
|
14
|
+
* something whose configuration says it should be running.
|
|
15
|
+
*
|
|
16
|
+
* Extracted because there were three copies of it — the Slack adapter, the
|
|
17
|
+
* control API, and the dashboard resolved from inside the control API —
|
|
18
|
+
* and a plugin loader would have been the fourth.
|
|
19
|
+
*/
|
|
20
|
+
export const loadOptionalModule = async (specifier, host) => {
|
|
21
|
+
try {
|
|
22
|
+
host.resolve(specifier);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
// Resolvable: any failure from here is a real problem with the installed
|
|
28
|
+
// package, and surfaces.
|
|
29
|
+
return (await host.import(specifier));
|
|
30
|
+
};
|
|
31
|
+
/** Where a package's own package.json is, given something it resolved to. */
|
|
32
|
+
const packageJsonFor = async (resolvedUrl, specifier) => {
|
|
33
|
+
let directory = path.dirname(fileURLToPath(resolvedUrl));
|
|
34
|
+
// Bounded rather than "until the filesystem root": a walk that reaches
|
|
35
|
+
// `/` would read some unrelated package.json and validate a manifest
|
|
36
|
+
// that never described this package.
|
|
37
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = await readFile(path.join(directory, 'package.json'), 'utf8');
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
const parent = path.dirname(directory);
|
|
44
|
+
if (parent === directory) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
directory = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new PluginManifestError(`Could not find a package.json for ${specifier}.`);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Whether a package's code is trusted — which is to say whether its
|
|
54
|
+
* manifest may declare a tool `safe`.
|
|
55
|
+
*
|
|
56
|
+
* First-party packages ship from this repository and pass its CI; anything
|
|
57
|
+
* else floors at `gated`. An operator can widen the set deliberately, and
|
|
58
|
+
* doing so is the same act as enabling the plugin at all: enablement is the
|
|
59
|
+
* security boundary, and this is a statement about how far it extends.
|
|
60
|
+
*/
|
|
61
|
+
export const isFirstPartyPackage = (packageName) => packageName === '@stratusagent' || packageName.startsWith('@stratusagent/');
|
|
62
|
+
const WORKSPACE_ROOT_KEY = 'workspaceRoot';
|
|
63
|
+
const configFor = (block, manifest, workspaceRoot) => {
|
|
64
|
+
const { enabled: _enabled, ...rest } = block;
|
|
65
|
+
const declaresWorkspace = Boolean(manifest.config
|
|
66
|
+
&& typeof manifest.config.properties === 'object'
|
|
67
|
+
&& manifest.config.properties !== null
|
|
68
|
+
&& !Array.isArray(manifest.config.properties)
|
|
69
|
+
&& WORKSPACE_ROOT_KEY in manifest.config.properties);
|
|
70
|
+
if (declaresWorkspace && workspaceRoot !== undefined && rest[WORKSPACE_ROOT_KEY] === undefined) {
|
|
71
|
+
return { ...rest, [WORKSPACE_ROOT_KEY]: workspaceRoot };
|
|
72
|
+
}
|
|
73
|
+
return rest;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Turn a `plugins` config block into running capability.
|
|
77
|
+
*
|
|
78
|
+
* Ordered the way the trust model requires: **nothing auto-loads** (only
|
|
79
|
+
* what is listed and enabled, and callers pass this only what a trusted
|
|
80
|
+
* config named), the **manifest is validated before the module is
|
|
81
|
+
* imported**, and `setup()` registers through the manifest-bound view
|
|
82
|
+
* rather than the raw registry.
|
|
83
|
+
*
|
|
84
|
+
* One plugin failing does not take the others — or the daemon — down: a
|
|
85
|
+
* failure is collected, named, and reported to the operator, because a
|
|
86
|
+
* daemon that refuses to start over a mistyped package name is a worse
|
|
87
|
+
* outcome than one running the plugins that do load and saying which did
|
|
88
|
+
* not. What is *never* degraded is the security half: an undeclared name, a
|
|
89
|
+
* collision, or a manifest that will not parse refuses that plugin whole.
|
|
90
|
+
*/
|
|
91
|
+
export const loadPlugins = async (options) => {
|
|
92
|
+
const entries = Object.entries(options.config ?? {});
|
|
93
|
+
const trusted = options.trusted ?? isFirstPartyPackage;
|
|
94
|
+
const loaded = [];
|
|
95
|
+
const failures = [];
|
|
96
|
+
// Which package owns each name so far, so a collision can name both.
|
|
97
|
+
const owners = new Map();
|
|
98
|
+
for (const [specifier, rawBlock] of entries) {
|
|
99
|
+
const block = rawBlock ?? {};
|
|
100
|
+
if (block.enabled === false) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Held outside the try so a plugin that was *constructed* and then
|
|
104
|
+
// failed can still be told to let go. A plugin acquires its resources
|
|
105
|
+
// in `createPlugin` and `setup` — a subscription on the bus, a socket,
|
|
106
|
+
// a child process — and a load that fails after that point leaves them
|
|
107
|
+
// held for the life of a daemon that goes on running without it.
|
|
108
|
+
let instance;
|
|
109
|
+
try {
|
|
110
|
+
const resolved = options.host.resolve(specifier);
|
|
111
|
+
const manifest = parsePluginManifest(await packageJsonFor(resolved, specifier), specifier);
|
|
112
|
+
const isTrusted = trusted(manifest.packageName);
|
|
113
|
+
// Validated *after* the host's defaults are folded in, because that
|
|
114
|
+
// is the configuration the plugin will actually be handed: a manifest
|
|
115
|
+
// that declares `workspaceRoot` required would otherwise be refused
|
|
116
|
+
// for missing the very setting the host supplies.
|
|
117
|
+
const config = configFor(block, manifest, options.workspaceRoot);
|
|
118
|
+
validatePluginConfig(manifest, config);
|
|
119
|
+
const module = (await options.host.import(specifier));
|
|
120
|
+
if (typeof module.createPlugin !== 'function') {
|
|
121
|
+
throw new PluginManifestError(`Plugin ${manifest.packageName} does not export createPlugin(config). See docs/architecture/plugins.md.`);
|
|
122
|
+
}
|
|
123
|
+
const plugin = await module.createPlugin(config);
|
|
124
|
+
if (!plugin || typeof plugin.setup !== 'function') {
|
|
125
|
+
throw new PluginManifestError(`Plugin ${manifest.packageName}: createPlugin did not return a plugin with a setup(context).`);
|
|
126
|
+
}
|
|
127
|
+
instance = plugin;
|
|
128
|
+
const view = new ManifestBoundToolRegistry({ manifest, target: options.tools, trusted: isTrusted });
|
|
129
|
+
await plugin.setup({ bus: options.bus, tools: view });
|
|
130
|
+
const tools = view.commit(owners);
|
|
131
|
+
loaded.push({
|
|
132
|
+
package: manifest.packageName,
|
|
133
|
+
name: typeof plugin.name === 'string' && plugin.name.length > 0 ? plugin.name : manifest.packageName,
|
|
134
|
+
manifest,
|
|
135
|
+
trusted: isTrusted,
|
|
136
|
+
tools,
|
|
137
|
+
instance: plugin,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Refused, and then released. A plugin whose `dispose` also throws is
|
|
142
|
+
// ignored: it is already being reported as failed, and the second
|
|
143
|
+
// failure would replace the reason that says why.
|
|
144
|
+
try {
|
|
145
|
+
await instance?.dispose?.();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Nothing more to do for a plugin that cannot even let go.
|
|
149
|
+
}
|
|
150
|
+
failures.push({
|
|
151
|
+
package: specifier,
|
|
152
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { loaded, failures };
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,yBAAyB,EAAyB,MAAM,eAAe,CAAC;AAkBjF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,SAAiB,EACjB,IAAwB,EACA,EAAE;IAC1B,IAAI,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,yEAAyE;IACzE,yBAAyB;IACzB,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAM,CAAC;AAC7C,CAAC,CAAC;AAKF,6EAA6E;AAC7E,MAAM,cAAc,GAAG,KAAK,EAAE,WAAmB,EAAE,SAAiB,EAAoB,EAAE;IACxF,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,uEAAuE;IACvE,qEAAqE;IACrE,qCAAqC;IACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC;YACzE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM;YACR,CAAC;YACD,SAAS,GAAG,MAAM,CAAC;QACrB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,mBAAmB,CAAC,qCAAqC,SAAS,GAAG,CAAC,CAAC;AACnF,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,WAAmB,EAAW,EAAE,CAClE,WAAW,KAAK,eAAe,IAAI,WAAW,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;AAgD9E,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAE3C,MAAM,SAAS,GAAG,CAChB,KAAiB,EACjB,QAAwB,EACxB,aAAiC,EACrB,EAAE;IACd,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAC7C,MAAM,iBAAiB,GAAG,OAAO,CAC/B,QAAQ,CAAC,MAAM;WACZ,OAAO,QAAQ,CAAC,MAAM,CAAC,UAAU,KAAK,QAAQ;WAC9C,QAAQ,CAAC,MAAM,CAAC,UAAU,KAAK,IAAI;WACnC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC;WAC1C,kBAAkB,IAAK,QAAQ,CAAC,MAAM,CAAC,UAAyB,CACpE,CAAC;IACF,IAAI,iBAAiB,IAAI,aAAa,KAAK,SAAS,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,SAAS,EAAE,CAAC;QAC/F,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,kBAAkB,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAA2B,EAA8B,EAAE;IAC3F,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,mBAAmB,CAAC;IACvD,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,qEAAqE;IACrE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEzC,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,QAAQ,IAAI,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC5B,SAAS;QACX,CAAC;QACD,mEAAmE;QACnE,sEAAsE;QACtE,uEAAuE;QACvE,uEAAuE;QACvE,iEAAiE;QACjE,IAAI,QAA4B,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACjD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;YAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAChD,oEAAoE;YACpE,sEAAsE;YACtE,oEAAoE;YACpE,kDAAkD;YAClD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;YACjE,oBAAoB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAEvC,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAoC,CAAC;YACzF,IAAI,OAAO,MAAM,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;gBAC9C,MAAM,IAAI,mBAAmB,CAC3B,UAAU,QAAQ,CAAC,WAAW,0EAA0E,CACzG,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACjD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBAClD,MAAM,IAAI,mBAAmB,CAC3B,UAAU,QAAQ,CAAC,WAAW,+DAA+D,CAC9F,CAAC;YACJ,CAAC;YACD,QAAQ,GAAG,MAAM,CAAC;YAElB,MAAM,IAAI,GAAG,IAAI,yBAAyB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YACpG,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAElC,MAAM,CAAC,IAAI,CAAC;gBACV,OAAO,EAAE,QAAQ,CAAC,WAAW;gBAC7B,IAAI,EAAE,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW;gBACpG,QAAQ;gBACR,OAAO,EAAE,SAAS;gBAClB,KAAK;gBACL,QAAQ,EAAE,MAAM;aACjB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,sEAAsE;YACtE,kEAAkE;YAClE,kDAAkD;YAClD,IAAI,CAAC;gBACH,MAAM,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;YAC7D,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC;gBACZ,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC/D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type JsonObject, type JsonValue, type ToolRisk } from '@stratusagent/core';
|
|
2
|
+
/**
|
|
3
|
+
* The manifest version this host understands. A plugin declaring anything
|
|
4
|
+
* else is refused rather than read optimistically: the whole point of the
|
|
5
|
+
* manifest is that the daemon can trust what it says about a package it has
|
|
6
|
+
* not imported, and reading an unknown dialect on a guess gives that up.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PLUGIN_MANIFEST_VERSION = 1;
|
|
9
|
+
/** A tool the manifest names outright. */
|
|
10
|
+
export interface PluginToolDeclaration {
|
|
11
|
+
name: string;
|
|
12
|
+
risk: ToolRisk;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A namespace the manifest claims instead of naming what is inside it —
|
|
16
|
+
* the form a bridge whose tool names exist only at connect time can be
|
|
17
|
+
* declared under. The risk is a ceiling on trust, never a floor the plugin
|
|
18
|
+
* picks: everything discovered under it registers at this risk or higher.
|
|
19
|
+
*/
|
|
20
|
+
export interface PluginNamespaceDeclaration {
|
|
21
|
+
namespace: string;
|
|
22
|
+
risk: ToolRisk;
|
|
23
|
+
}
|
|
24
|
+
export interface PluginSkillDeclaration {
|
|
25
|
+
id: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PluginContributions {
|
|
29
|
+
tools: PluginToolDeclaration[];
|
|
30
|
+
toolsDiscovered: PluginNamespaceDeclaration[];
|
|
31
|
+
/** Parsed and reported, but not yet registrable — skills are step 09. */
|
|
32
|
+
skills: PluginSkillDeclaration[];
|
|
33
|
+
}
|
|
34
|
+
export interface PluginManifest {
|
|
35
|
+
/** The package name, which is the plugin's identity. */
|
|
36
|
+
packageName: string;
|
|
37
|
+
pluginVersion: number;
|
|
38
|
+
contributes: PluginContributions;
|
|
39
|
+
credentials: string[];
|
|
40
|
+
/** JSON Schema for the plugin's own config block, if it declared one. */
|
|
41
|
+
config?: JsonObject;
|
|
42
|
+
}
|
|
43
|
+
/** A manifest that cannot be trusted to describe the package it came from. */
|
|
44
|
+
export declare class PluginManifestError extends Error {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
47
|
+
/** A config block that does not match what the manifest said it would be. */
|
|
48
|
+
export declare class PluginConfigError extends Error {
|
|
49
|
+
constructor(message: string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read the `stratus` field of a package.json into a manifest, or refuse it.
|
|
53
|
+
*
|
|
54
|
+
* Nothing here imports the package — that is the property the manifest
|
|
55
|
+
* exists for. A misconfigured or over-reaching plugin fails at load, with
|
|
56
|
+
* its own name in the message, rather than at the first tool call in the
|
|
57
|
+
* middle of somebody's turn.
|
|
58
|
+
*/
|
|
59
|
+
export declare const parsePluginManifest: (packageJson: unknown, specifier: string) => PluginManifest;
|
|
60
|
+
/**
|
|
61
|
+
* The risk a manifest says a tool registers at, or undefined when the
|
|
62
|
+
* manifest does not authorize the name at all.
|
|
63
|
+
*
|
|
64
|
+
* A literal declaration wins over a namespace: a plugin that both owns
|
|
65
|
+
* `mcp.*` at `gated` and names `mcp.ping` at `safe` gets what it wrote for
|
|
66
|
+
* the name it wrote. Namespace matching is `matchesToolAllowlist` — the
|
|
67
|
+
* same reading of `mcp.*` a soul's allowlist gets, because two readings of
|
|
68
|
+
* one glob is two answers to what a package may register.
|
|
69
|
+
*/
|
|
70
|
+
export declare const declaredRiskFor: (manifest: PluginManifest, toolName: string) => ToolRisk | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Keys the host owns inside a plugin's config block. They are stripped
|
|
73
|
+
* before the plugin's own schema sees the block — a plugin author writes a
|
|
74
|
+
* schema for their settings, not for the machinery that selects them.
|
|
75
|
+
*/
|
|
76
|
+
export declare const HOST_CONFIG_KEYS: readonly ["enabled", "agents"];
|
|
77
|
+
/**
|
|
78
|
+
* Check a config block against the manifest's schema.
|
|
79
|
+
*
|
|
80
|
+
* A deliberately small subset of JSON Schema — `type`, `properties`,
|
|
81
|
+
* `required`, `items`, `enum`, and `additionalProperties: false` — chosen
|
|
82
|
+
* so that the daemon can answer "is this configuration well-formed" without
|
|
83
|
+
* carrying a validator library into every install. Keywords outside the
|
|
84
|
+
* subset are ignored rather than refused, so a schema written against the
|
|
85
|
+
* full standard still loads; what it buys is fewer checks, never a false
|
|
86
|
+
* pass on a keyword we pretended to understand.
|
|
87
|
+
*/
|
|
88
|
+
export declare const validateAgainstSchema: (value: JsonValue, schema: JsonObject, where: string, options?: {
|
|
89
|
+
requireRequired?: boolean;
|
|
90
|
+
}) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Validate one plugin's whole config block — the defaults and every
|
|
93
|
+
* per-agent override — against the manifest's schema.
|
|
94
|
+
*
|
|
95
|
+
* The per-agent entries are checked with `required` relaxed, and that is
|
|
96
|
+
* the point of them: an agent block says what differs for that agent, so
|
|
97
|
+
* holding a two-key override to a schema's required list would make the
|
|
98
|
+
* defaults above it unusable.
|
|
99
|
+
*/
|
|
100
|
+
export declare const validatePluginConfig: (manifest: PluginManifest, block: JsonObject) => void;
|
|
101
|
+
/**
|
|
102
|
+
* One agent's settings for a plugin: its own entry over the defaults, key
|
|
103
|
+
* by key — the shape `approvals` already uses, and the reason a plugin
|
|
104
|
+
* whose settings are an access boundary must call this **per execution**
|
|
105
|
+
* rather than closing over a value at setup. `session.agent.id` is what
|
|
106
|
+
* makes the answer different for two agents; resolving once at setup gives
|
|
107
|
+
* every agent whichever one happened to be asked about first.
|
|
108
|
+
*/
|
|
109
|
+
export declare const resolvePluginAgentConfig: (block: JsonObject | undefined, agentId: string) => JsonObject;
|
|
110
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,eAAe,EAAE,0BAA0B,EAAE,CAAC;IAC9C,yEAAyE;IACzE,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,mBAAmB,CAAC;IACjC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,yEAAyE;IACzE,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,8EAA8E;AAC9E,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,6EAA6E;AAC7E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AA6BD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAI,aAAa,OAAO,EAAE,WAAW,MAAM,KAAG,cAyG7E,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,cAAc,EAAE,UAAU,MAAM,KAAG,QAAQ,GAAG,SASvF,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,gCAAiC,CAAC;AAY/D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,SAAS,EAChB,QAAQ,UAAU,EAClB,OAAO,MAAM,EACb,UAAS;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,KAC1C,IA6CF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,UAAU,cAAc,EAAE,OAAO,UAAU,KAAG,IA6BlF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,UAAU,GAAG,SAAS,EAAE,SAAS,MAAM,KAAG,UAOzF,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { DEFAULT_TOOL_RISK, matchesToolAllowlist, } from '@stratusagent/core';
|
|
2
|
+
/**
|
|
3
|
+
* The manifest version this host understands. A plugin declaring anything
|
|
4
|
+
* else is refused rather than read optimistically: the whole point of the
|
|
5
|
+
* manifest is that the daemon can trust what it says about a package it has
|
|
6
|
+
* not imported, and reading an unknown dialect on a guess gives that up.
|
|
7
|
+
*/
|
|
8
|
+
export const PLUGIN_MANIFEST_VERSION = 1;
|
|
9
|
+
/** A manifest that cannot be trusted to describe the package it came from. */
|
|
10
|
+
export class PluginManifestError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'PluginManifestError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** A config block that does not match what the manifest said it would be. */
|
|
17
|
+
export class PluginConfigError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'PluginConfigError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// `namespace.verb`, with the nesting a bridged server's names need
|
|
24
|
+
// (`mcp.linear.create_issue`). Underscores are allowed inside a segment
|
|
25
|
+
// because somebody else's API chose those names; the leading character is
|
|
26
|
+
// not, so nothing can be typed to look like a flag or a hidden file.
|
|
27
|
+
const TOOL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9_-]*)+$/;
|
|
28
|
+
const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9_-]*)*\.\*$/;
|
|
29
|
+
const SKILL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
30
|
+
const RISKS = ['safe', 'gated', 'dangerous'];
|
|
31
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
const parseRisk = (raw, where) => {
|
|
33
|
+
// Undeclared is `gated`, exactly as it is on a Tool: a manifest entry
|
|
34
|
+
// that forgot to think about risk is held back rather than waved through.
|
|
35
|
+
if (raw === undefined) {
|
|
36
|
+
return DEFAULT_TOOL_RISK;
|
|
37
|
+
}
|
|
38
|
+
if (typeof raw !== 'string' || !RISKS.includes(raw)) {
|
|
39
|
+
throw new PluginManifestError(`Invalid risk in ${where}: ${JSON.stringify(raw)}. Use safe, gated, or dangerous.`);
|
|
40
|
+
}
|
|
41
|
+
return raw;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read the `stratus` field of a package.json into a manifest, or refuse it.
|
|
45
|
+
*
|
|
46
|
+
* Nothing here imports the package — that is the property the manifest
|
|
47
|
+
* exists for. A misconfigured or over-reaching plugin fails at load, with
|
|
48
|
+
* its own name in the message, rather than at the first tool call in the
|
|
49
|
+
* middle of somebody's turn.
|
|
50
|
+
*/
|
|
51
|
+
export const parsePluginManifest = (packageJson, specifier) => {
|
|
52
|
+
if (!isObject(packageJson)) {
|
|
53
|
+
throw new PluginManifestError(`Package ${specifier} has no readable package.json.`);
|
|
54
|
+
}
|
|
55
|
+
const packageName = typeof packageJson.name === 'string' && packageJson.name.length > 0
|
|
56
|
+
? packageJson.name
|
|
57
|
+
: specifier;
|
|
58
|
+
const stratus = packageJson.stratus;
|
|
59
|
+
if (!isObject(stratus)) {
|
|
60
|
+
throw new PluginManifestError(`Package ${packageName} is not a Stratus plugin: its package.json has no "stratus" manifest.`);
|
|
61
|
+
}
|
|
62
|
+
const version = stratus.pluginVersion;
|
|
63
|
+
if (version !== PLUGIN_MANIFEST_VERSION) {
|
|
64
|
+
throw new PluginManifestError(`Plugin ${packageName} declares pluginVersion ${JSON.stringify(version)}; this host understands ${PLUGIN_MANIFEST_VERSION}.`);
|
|
65
|
+
}
|
|
66
|
+
const contributes = isObject(stratus.contributes) ? stratus.contributes : {};
|
|
67
|
+
const tools = [];
|
|
68
|
+
const toolsDiscovered = [];
|
|
69
|
+
const skills = [];
|
|
70
|
+
if (contributes.tools !== undefined) {
|
|
71
|
+
if (!Array.isArray(contributes.tools)) {
|
|
72
|
+
throw new PluginManifestError(`Plugin ${packageName}: contributes.tools must be an array.`);
|
|
73
|
+
}
|
|
74
|
+
for (const entry of contributes.tools) {
|
|
75
|
+
if (!isObject(entry) || typeof entry.name !== 'string') {
|
|
76
|
+
throw new PluginManifestError(`Plugin ${packageName}: every contributes.tools entry needs a "name".`);
|
|
77
|
+
}
|
|
78
|
+
if (!TOOL_NAME_PATTERN.test(entry.name)) {
|
|
79
|
+
throw new PluginManifestError(`Plugin ${packageName}: ${JSON.stringify(entry.name)} is not a tool name. Tools are namespace.verb, lowercase (fs.read).`);
|
|
80
|
+
}
|
|
81
|
+
tools.push({ name: entry.name, risk: parseRisk(entry.risk, `${packageName} tool ${entry.name}`) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (contributes.toolsDiscovered !== undefined) {
|
|
85
|
+
if (!Array.isArray(contributes.toolsDiscovered)) {
|
|
86
|
+
throw new PluginManifestError(`Plugin ${packageName}: contributes.toolsDiscovered must be an array.`);
|
|
87
|
+
}
|
|
88
|
+
for (const entry of contributes.toolsDiscovered) {
|
|
89
|
+
if (!isObject(entry) || typeof entry.namespace !== 'string') {
|
|
90
|
+
throw new PluginManifestError(`Plugin ${packageName}: every contributes.toolsDiscovered entry needs a "namespace".`);
|
|
91
|
+
}
|
|
92
|
+
if (!NAMESPACE_PATTERN.test(entry.namespace)) {
|
|
93
|
+
throw new PluginManifestError(`Plugin ${packageName}: ${JSON.stringify(entry.namespace)} is not a namespace. Declare one as a prefix and a star (mcp.*).`);
|
|
94
|
+
}
|
|
95
|
+
toolsDiscovered.push({
|
|
96
|
+
namespace: entry.namespace,
|
|
97
|
+
risk: parseRisk(entry.risk, `${packageName} namespace ${entry.namespace}`),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (contributes.skills !== undefined) {
|
|
102
|
+
if (!Array.isArray(contributes.skills)) {
|
|
103
|
+
throw new PluginManifestError(`Plugin ${packageName}: contributes.skills must be an array.`);
|
|
104
|
+
}
|
|
105
|
+
for (const entry of contributes.skills) {
|
|
106
|
+
if (!isObject(entry) || typeof entry.id !== 'string' || typeof entry.path !== 'string') {
|
|
107
|
+
throw new PluginManifestError(`Plugin ${packageName}: every contributes.skills entry needs an "id" and a "path".`);
|
|
108
|
+
}
|
|
109
|
+
if (!SKILL_ID_PATTERN.test(entry.id)) {
|
|
110
|
+
throw new PluginManifestError(`Plugin ${packageName}: ${JSON.stringify(entry.id)} is not a skill id. Skill ids are kebab-case (web-research).`);
|
|
111
|
+
}
|
|
112
|
+
skills.push({ id: entry.id, path: entry.path });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const credentials = [];
|
|
116
|
+
if (stratus.credentials !== undefined) {
|
|
117
|
+
if (!Array.isArray(stratus.credentials) || stratus.credentials.some((name) => typeof name !== 'string')) {
|
|
118
|
+
throw new PluginManifestError(`Plugin ${packageName}: credentials must be an array of names.`);
|
|
119
|
+
}
|
|
120
|
+
credentials.push(...stratus.credentials);
|
|
121
|
+
}
|
|
122
|
+
if (stratus.config !== undefined && !isObject(stratus.config)) {
|
|
123
|
+
throw new PluginManifestError(`Plugin ${packageName}: config must be a JSON Schema object.`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
packageName,
|
|
127
|
+
pluginVersion: PLUGIN_MANIFEST_VERSION,
|
|
128
|
+
contributes: { tools, toolsDiscovered, skills },
|
|
129
|
+
credentials,
|
|
130
|
+
...(isObject(stratus.config) ? { config: stratus.config } : {}),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The risk a manifest says a tool registers at, or undefined when the
|
|
135
|
+
* manifest does not authorize the name at all.
|
|
136
|
+
*
|
|
137
|
+
* A literal declaration wins over a namespace: a plugin that both owns
|
|
138
|
+
* `mcp.*` at `gated` and names `mcp.ping` at `safe` gets what it wrote for
|
|
139
|
+
* the name it wrote. Namespace matching is `matchesToolAllowlist` — the
|
|
140
|
+
* same reading of `mcp.*` a soul's allowlist gets, because two readings of
|
|
141
|
+
* one glob is two answers to what a package may register.
|
|
142
|
+
*/
|
|
143
|
+
export const declaredRiskFor = (manifest, toolName) => {
|
|
144
|
+
const literal = manifest.contributes.tools.find((entry) => entry.name === toolName);
|
|
145
|
+
if (literal) {
|
|
146
|
+
return literal.risk;
|
|
147
|
+
}
|
|
148
|
+
const namespace = manifest.contributes.toolsDiscovered.find((entry) => matchesToolAllowlist(toolName, [entry.namespace]));
|
|
149
|
+
return namespace?.risk;
|
|
150
|
+
};
|
|
151
|
+
// ---- config validation -----------------------------------------------------
|
|
152
|
+
/**
|
|
153
|
+
* Keys the host owns inside a plugin's config block. They are stripped
|
|
154
|
+
* before the plugin's own schema sees the block — a plugin author writes a
|
|
155
|
+
* schema for their settings, not for the machinery that selects them.
|
|
156
|
+
*/
|
|
157
|
+
export const HOST_CONFIG_KEYS = ['enabled', 'agents'];
|
|
158
|
+
const typeOf = (value) => {
|
|
159
|
+
if (value === null)
|
|
160
|
+
return 'null';
|
|
161
|
+
if (Array.isArray(value))
|
|
162
|
+
return 'array';
|
|
163
|
+
if (Number.isInteger(value))
|
|
164
|
+
return 'integer';
|
|
165
|
+
return typeof value;
|
|
166
|
+
};
|
|
167
|
+
const typeMatches = (expected, actual) => expected === actual || (expected === 'number' && actual === 'integer');
|
|
168
|
+
/**
|
|
169
|
+
* Check a config block against the manifest's schema.
|
|
170
|
+
*
|
|
171
|
+
* A deliberately small subset of JSON Schema — `type`, `properties`,
|
|
172
|
+
* `required`, `items`, `enum`, and `additionalProperties: false` — chosen
|
|
173
|
+
* so that the daemon can answer "is this configuration well-formed" without
|
|
174
|
+
* carrying a validator library into every install. Keywords outside the
|
|
175
|
+
* subset are ignored rather than refused, so a schema written against the
|
|
176
|
+
* full standard still loads; what it buys is fewer checks, never a false
|
|
177
|
+
* pass on a keyword we pretended to understand.
|
|
178
|
+
*/
|
|
179
|
+
export const validateAgainstSchema = (value, schema, where, options = {}) => {
|
|
180
|
+
const requireRequired = options.requireRequired ?? true;
|
|
181
|
+
const allowedValues = Array.isArray(schema.enum) ? schema.enum : undefined;
|
|
182
|
+
if (allowedValues && !allowedValues.some((allowed) => JSON.stringify(allowed) === JSON.stringify(value))) {
|
|
183
|
+
throw new PluginConfigError(`${where} must be one of ${allowedValues.map((entry) => JSON.stringify(entry)).join(', ')}; got ${JSON.stringify(value)}.`);
|
|
184
|
+
}
|
|
185
|
+
if (typeof schema.type === 'string' && !typeMatches(schema.type, typeOf(value))) {
|
|
186
|
+
throw new PluginConfigError(`${where} must be a ${schema.type}; got ${typeOf(value)}.`);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(value) && schema.items !== undefined && isObject(schema.items)) {
|
|
189
|
+
value.forEach((entry, index) => {
|
|
190
|
+
validateAgainstSchema(entry, schema.items, `${where}[${index}]`, options);
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (isObject(value) && !Array.isArray(value)) {
|
|
195
|
+
const properties = isObject(schema.properties) ? schema.properties : undefined;
|
|
196
|
+
if (requireRequired && Array.isArray(schema.required)) {
|
|
197
|
+
for (const key of schema.required) {
|
|
198
|
+
if (typeof key === 'string' && !(key in value)) {
|
|
199
|
+
throw new PluginConfigError(`${where} is missing required setting ${JSON.stringify(key)}.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (properties) {
|
|
204
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
205
|
+
const propertySchema = properties[key];
|
|
206
|
+
if (propertySchema === undefined) {
|
|
207
|
+
if (schema.additionalProperties === false) {
|
|
208
|
+
throw new PluginConfigError(`${where} has no setting named ${JSON.stringify(key)}.`);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (isObject(propertySchema)) {
|
|
213
|
+
validateAgainstSchema(entry, propertySchema, `${where}.${key}`, options);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Validate one plugin's whole config block — the defaults and every
|
|
221
|
+
* per-agent override — against the manifest's schema.
|
|
222
|
+
*
|
|
223
|
+
* The per-agent entries are checked with `required` relaxed, and that is
|
|
224
|
+
* the point of them: an agent block says what differs for that agent, so
|
|
225
|
+
* holding a two-key override to a schema's required list would make the
|
|
226
|
+
* defaults above it unusable.
|
|
227
|
+
*/
|
|
228
|
+
export const validatePluginConfig = (manifest, block) => {
|
|
229
|
+
const schema = manifest.config;
|
|
230
|
+
if (!schema) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const { enabled: _enabled, agents, ...defaults } = block;
|
|
234
|
+
validateAgainstSchema(defaults, schema, `plugins["${manifest.packageName}"]`);
|
|
235
|
+
if (agents === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!isObject(agents)) {
|
|
239
|
+
throw new PluginConfigError(`plugins["${manifest.packageName}"].agents must be an object keyed by agent id.`);
|
|
240
|
+
}
|
|
241
|
+
for (const [agentId, entry] of Object.entries(agents)) {
|
|
242
|
+
if (!isObject(entry)) {
|
|
243
|
+
throw new PluginConfigError(`plugins["${manifest.packageName}"].agents.${agentId} must be an object of settings.`);
|
|
244
|
+
}
|
|
245
|
+
validateAgainstSchema(entry, schema, `plugins["${manifest.packageName}"].agents.${agentId}`, { requireRequired: false });
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* One agent's settings for a plugin: its own entry over the defaults, key
|
|
250
|
+
* by key — the shape `approvals` already uses, and the reason a plugin
|
|
251
|
+
* whose settings are an access boundary must call this **per execution**
|
|
252
|
+
* rather than closing over a value at setup. `session.agent.id` is what
|
|
253
|
+
* makes the answer different for two agents; resolving once at setup gives
|
|
254
|
+
* every agent whichever one happened to be asked about first.
|
|
255
|
+
*/
|
|
256
|
+
export const resolvePluginAgentConfig = (block, agentId) => {
|
|
257
|
+
if (!block) {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
const { enabled: _enabled, agents, ...defaults } = block;
|
|
261
|
+
const overrides = isObject(agents) ? agents[agentId] : undefined;
|
|
262
|
+
return isObject(overrides) ? { ...defaults, ...overrides } : { ...defaults };
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ToolRegistry, type Tool, type ToolDescriptor, type ToolRisk } from '@stratusagent/core';
|
|
2
|
+
import { type PluginManifest } from './manifest.ts';
|
|
3
|
+
/** What the daemon knows about one registered tool once a plugin is loaded. */
|
|
4
|
+
export interface PluginToolRecord {
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
risk: ToolRisk;
|
|
8
|
+
/** The package whose code this tool is — provenance, kept for risk questions. */
|
|
9
|
+
package: string;
|
|
10
|
+
/** Whether that package is inside the trusted set (see the risk floor). */
|
|
11
|
+
trusted: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The risk floor a package's tools cannot register below.
|
|
15
|
+
*
|
|
16
|
+
* `safe` means "run this unattended, with nobody watching", and it is not a
|
|
17
|
+
* claim the code being judged gets to make about itself. First-party
|
|
18
|
+
* packages ship in this repository and are gated by its CI, so their
|
|
19
|
+
* manifests are read as written; everything else floors at `gated`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const riskFloorFor: (trusted: boolean) => ToolRisk;
|
|
22
|
+
/**
|
|
23
|
+
* The manifest-bound view a plugin's `setup()` registers through.
|
|
24
|
+
*
|
|
25
|
+
* Validating package.json before importing says what a plugin *claims*; it
|
|
26
|
+
* says nothing about what `setup()` then does, and `ToolRegistry.register`
|
|
27
|
+
* is a bare `Map.set` that records neither the originating package nor
|
|
28
|
+
* anything to check a claim against. Handed the raw registry, a plugin
|
|
29
|
+
* could register a tool it never declared, mark it `safe`, and run
|
|
30
|
+
* unattended — which would make the floor decoration. So this view rejects
|
|
31
|
+
* a name the manifest does not declare, applies the declared risk and the
|
|
32
|
+
* floor rather than trusting the object it is handed, and keeps the package
|
|
33
|
+
* as provenance.
|
|
34
|
+
*
|
|
35
|
+
* Registrations are **staged, not committed**. A plugin that registers
|
|
36
|
+
* three tools and then throws — or whose fourth name collides with another
|
|
37
|
+
* package's — must not leave the first three live in a shared registry, so
|
|
38
|
+
* nothing reaches the target until `commit()` says the whole plugin loaded.
|
|
39
|
+
*/
|
|
40
|
+
export declare class ManifestBoundToolRegistry extends ToolRegistry {
|
|
41
|
+
private readonly manifest;
|
|
42
|
+
private readonly target;
|
|
43
|
+
private readonly trusted;
|
|
44
|
+
private readonly staged;
|
|
45
|
+
private readonly records;
|
|
46
|
+
constructor(options: {
|
|
47
|
+
manifest: PluginManifest;
|
|
48
|
+
target: ToolRegistry;
|
|
49
|
+
trusted: boolean;
|
|
50
|
+
});
|
|
51
|
+
register(tool: Tool): void;
|
|
52
|
+
/** Reads see the whole registry: a plugin composing on another's tool is not the boundary here. */
|
|
53
|
+
get(name: string): Tool | undefined;
|
|
54
|
+
list(): Tool[];
|
|
55
|
+
describe(): ToolDescriptor[];
|
|
56
|
+
/**
|
|
57
|
+
* Move this plugin's tools into the shared registry, or refuse the whole
|
|
58
|
+
* plugin. A name already registered is a load-time error naming both
|
|
59
|
+
* packages, never a silent overwrite — the same reason a duplicate agent
|
|
60
|
+
* id refuses the roster rather than picking whichever loaded last.
|
|
61
|
+
*/
|
|
62
|
+
commit(owners: Map<string, string>): PluginToolRecord[];
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,YAAY,EACZ,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE1F,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,QAAwC,CAAC;AAEzF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,yBAA0B,SAAQ,YAAY;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAE1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAElD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;gBAEtC,OAAO,EAAE;QAAE,QAAQ,EAAE,cAAc,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE;IAOhF,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAuCnC,mGAAmG;IAC1F,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAInC,IAAI,IAAI,IAAI,EAAE;IAId,QAAQ,IAAI,cAAc,EAAE;IASrC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB,EAAE;CAsBxD"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { raiseRiskTo, resolveToolRisk, ToolRegistry, } from '@stratusagent/core';
|
|
2
|
+
import { declaredRiskFor, PluginManifestError } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The risk floor a package's tools cannot register below.
|
|
5
|
+
*
|
|
6
|
+
* `safe` means "run this unattended, with nobody watching", and it is not a
|
|
7
|
+
* claim the code being judged gets to make about itself. First-party
|
|
8
|
+
* packages ship in this repository and are gated by its CI, so their
|
|
9
|
+
* manifests are read as written; everything else floors at `gated`.
|
|
10
|
+
*/
|
|
11
|
+
export const riskFloorFor = (trusted) => (trusted ? 'safe' : 'gated');
|
|
12
|
+
/**
|
|
13
|
+
* The manifest-bound view a plugin's `setup()` registers through.
|
|
14
|
+
*
|
|
15
|
+
* Validating package.json before importing says what a plugin *claims*; it
|
|
16
|
+
* says nothing about what `setup()` then does, and `ToolRegistry.register`
|
|
17
|
+
* is a bare `Map.set` that records neither the originating package nor
|
|
18
|
+
* anything to check a claim against. Handed the raw registry, a plugin
|
|
19
|
+
* could register a tool it never declared, mark it `safe`, and run
|
|
20
|
+
* unattended — which would make the floor decoration. So this view rejects
|
|
21
|
+
* a name the manifest does not declare, applies the declared risk and the
|
|
22
|
+
* floor rather than trusting the object it is handed, and keeps the package
|
|
23
|
+
* as provenance.
|
|
24
|
+
*
|
|
25
|
+
* Registrations are **staged, not committed**. A plugin that registers
|
|
26
|
+
* three tools and then throws — or whose fourth name collides with another
|
|
27
|
+
* package's — must not leave the first three live in a shared registry, so
|
|
28
|
+
* nothing reaches the target until `commit()` says the whole plugin loaded.
|
|
29
|
+
*/
|
|
30
|
+
export class ManifestBoundToolRegistry extends ToolRegistry {
|
|
31
|
+
manifest;
|
|
32
|
+
target;
|
|
33
|
+
trusted;
|
|
34
|
+
staged = new Map();
|
|
35
|
+
records = [];
|
|
36
|
+
constructor(options) {
|
|
37
|
+
super();
|
|
38
|
+
this.manifest = options.manifest;
|
|
39
|
+
this.target = options.target;
|
|
40
|
+
this.trusted = options.trusted;
|
|
41
|
+
}
|
|
42
|
+
register(tool) {
|
|
43
|
+
const declared = declaredRiskFor(this.manifest, tool.name);
|
|
44
|
+
if (declared === undefined) {
|
|
45
|
+
throw new PluginManifestError(`Plugin ${this.manifest.packageName} tried to register ${tool.name}, which its manifest does not declare. `
|
|
46
|
+
+ 'Add it to contributes.tools, or declare the namespace it belongs to in contributes.toolsDiscovered.');
|
|
47
|
+
}
|
|
48
|
+
if (this.staged.has(tool.name)) {
|
|
49
|
+
throw new PluginManifestError(`Plugin ${this.manifest.packageName} registered ${tool.name} twice.`);
|
|
50
|
+
}
|
|
51
|
+
// Three claims, and the effective risk is the riskiest of them: the
|
|
52
|
+
// manifest's declaration, the floor its package is held to, and
|
|
53
|
+
// whatever the object says about itself. The object can raise itself
|
|
54
|
+
// and can never talk its way below what its own package declared.
|
|
55
|
+
const claimed = tool.risk;
|
|
56
|
+
const risk = raiseRiskTo(raiseRiskTo(declared, riskFloorFor(this.trusted)), claimed ?? 'safe');
|
|
57
|
+
// Prototype preserved: a tool may be a class instance or carry the
|
|
58
|
+
// `runtime`/`createCommand` shape the local-command executor detects,
|
|
59
|
+
// and a plain spread would quietly turn either into an object the
|
|
60
|
+
// executor no longer recognises.
|
|
61
|
+
const bound = Object.assign(Object.create(Object.getPrototypeOf(tool)), tool, { risk });
|
|
62
|
+
this.staged.set(tool.name, bound);
|
|
63
|
+
this.records.push({
|
|
64
|
+
name: tool.name,
|
|
65
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
66
|
+
risk,
|
|
67
|
+
package: this.manifest.packageName,
|
|
68
|
+
trusted: this.trusted,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Reads see the whole registry: a plugin composing on another's tool is not the boundary here. */
|
|
72
|
+
get(name) {
|
|
73
|
+
return this.staged.get(name) ?? this.target.get(name);
|
|
74
|
+
}
|
|
75
|
+
list() {
|
|
76
|
+
return [...this.target.list(), ...this.staged.values()];
|
|
77
|
+
}
|
|
78
|
+
describe() {
|
|
79
|
+
return this.list().map((tool) => ({
|
|
80
|
+
name: tool.name,
|
|
81
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
82
|
+
...(tool.parameters ? { parameters: tool.parameters } : {}),
|
|
83
|
+
risk: resolveToolRisk(tool),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Move this plugin's tools into the shared registry, or refuse the whole
|
|
88
|
+
* plugin. A name already registered is a load-time error naming both
|
|
89
|
+
* packages, never a silent overwrite — the same reason a duplicate agent
|
|
90
|
+
* id refuses the roster rather than picking whichever loaded last.
|
|
91
|
+
*/
|
|
92
|
+
commit(owners) {
|
|
93
|
+
for (const name of this.staged.keys()) {
|
|
94
|
+
const existing = owners.get(name);
|
|
95
|
+
if (existing !== undefined) {
|
|
96
|
+
throw new PluginManifestError(`Tool name collision: ${name} is contributed by both ${existing} and ${this.manifest.packageName}. `
|
|
97
|
+
+ 'Disable one of them; a tool name is unique per install.');
|
|
98
|
+
}
|
|
99
|
+
if (this.target.get(name)) {
|
|
100
|
+
throw new PluginManifestError(`Tool name collision: ${name} is already registered by the kernel. `
|
|
101
|
+
+ `Plugin ${this.manifest.packageName} cannot replace it.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const [name, tool] of this.staged) {
|
|
105
|
+
this.target.register(tool);
|
|
106
|
+
owners.set(name, this.manifest.packageName);
|
|
107
|
+
}
|
|
108
|
+
return [...this.records];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stratusagent/plugins",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.13 <23 || >=23.4"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@stratusagent/core": "0.6.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.6.0",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/stratuslabs/agent.git",
|
|
31
|
+
"directory": "packages/plugins"
|
|
32
|
+
},
|
|
33
|
+
"description": "The Stratus Agent plugin host: manifest validation, the manifest-bound registration view, and the loader that turns a config block into running capability",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"stratus",
|
|
36
|
+
"agent",
|
|
37
|
+
"ai",
|
|
38
|
+
"llm",
|
|
39
|
+
"plugins"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -b",
|
|
46
|
+
"typecheck": "tsc --noEmit --project tsconfig.test.json",
|
|
47
|
+
"test": "node --test --experimental-strip-types"
|
|
48
|
+
}
|
|
49
|
+
}
|