@monotykamary/dsh 0.1.0-rc.5
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.i18n.yaml +6 -0
- package/README.md +47 -0
- package/README.zh.md +47 -0
- package/config/agent-presets/code/agent.cordis.yml +262 -0
- package/config/agent-presets/code/preset.yml +3 -0
- package/config/agent-presets/cordis/agent.cordis.yml +262 -0
- package/config/agent-presets/cordis/preset.yml +3 -0
- package/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md +420 -0
- package/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +154 -0
- package/config/agent-presets/minimal/agent.cordis.yml +62 -0
- package/config/agent-presets/minimal/preset.yml +3 -0
- package/config/agent-presets/standard/agent.cordis.yml +251 -0
- package/config/agent-presets/standard/preset.yml +3 -0
- package/lib/bin.js +154 -0
- package/lib/dump-config-B1FRQWhz.js +52 -0
- package/lib/plugin-CAzgdPE6.js +129 -0
- package/lib/profile-boot-BJ6jgmCd.js +283 -0
- package/lib/profile-boot-DEALHArV.js +2 -0
- package/package.json +101 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cordis-plugin-development
|
|
3
|
+
description: Create, modify, debug, or extend dynamic Cordis Plugins, including Host Services and Events, Client Slot and theme UI, Package-private Client-to-Host calls, dynamic Tools, version updates, approval failures, and runtime diagnostics. Use this Skill to route a user request to the correct platform and Inspect Provider, then define, run, repair, or roll back the Plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Develop Dynamic Cordis Plugins
|
|
7
|
+
|
|
8
|
+
First determine whether a capability belongs on Host or Client, then query the real interface before writing code. Never infer a complete API from a Service name, Event payload, Slot props, theme token, or example.
|
|
9
|
+
|
|
10
|
+
## Standard workflow
|
|
11
|
+
|
|
12
|
+
1. Call `cordis_inspect_list` to obtain the Providers, methods, and schemas currently registered on Host and Client.
|
|
13
|
+
2. Select the smallest set of `cordis_inspect_query` calls needed to read the exact Services, Events, Builtins, Slots, Theme tokens, or Tools that the implementation will use.
|
|
14
|
+
3. For a new Plugin, design its first Package. To modify an existing Plugin, first use `cordis_inspect_self(pluginId, packageId)` to read the base source and diagnostics.
|
|
15
|
+
4. Write plain JavaScript in `code.host`, `code.client`, or both, then call `cordis_define`.
|
|
16
|
+
5. Call `cordis_run` with the final `pluginId` and `packageId` returned by define.
|
|
17
|
+
6. Handle approval, waiting, Client loading, and render failures from the Run card, steering messages, or `cordis_inspect_self`.
|
|
18
|
+
7. Use `cordis_stop` to disable the Plugin temporarily. Use `cordis_undefine` only when it is no longer needed.
|
|
19
|
+
|
|
20
|
+
Do not wait in the same turn for user approval or asynchronous browser results. After `cordis_run` returns `awaiting-approval` or `starting`, end the current Tool flow and wait for the system to report the final outcome through state updates and steering.
|
|
21
|
+
|
|
22
|
+
## Tool usage guidance
|
|
23
|
+
|
|
24
|
+
| Tool | Use it when | Do not |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `cordis_inspect_list` | Discover current Host/Client Providers and method schemas in one call; refresh after the runtime capability directory changes | Hard-code Provider names and skip list; treat a manifest as business data |
|
|
27
|
+
| `cordis_inspect_query` | Confirm exact Service methods, Event modes, Builtins, Slots, tokens, or Tool schemas before writing code | Use it instead of calling a real Service from the Plugin; assume a Client query will finish without a responding page |
|
|
28
|
+
| `cordis_inspect_self` | List current Plugins, inspect version pointers, or read exact Package source and runtime diagnostics | Fetch all source just to build a list; use it to modify or start a Plugin |
|
|
29
|
+
| `cordis_define` | Create a Plugin's first version or append an immutable Package to an existing Plugin; let the user preview the code first | Expect define to execute `apply`, request approval, or update current |
|
|
30
|
+
| `cordis_run` | Activate an exact Package; use `run` for first activation, restart, or rollback, and `update` to switch versions | Use `run` to switch versions implicitly; treat pending or starting as success |
|
|
31
|
+
| `cordis_stop` | Pause current effects while preserving Packages, grants, and version pointers for later use | Use stop to mean permanent deletion |
|
|
32
|
+
| `cordis_undefine` | Permanently remove a Plugin and all of its Packages and clear historical business views | Call it while rollback, inspection, or restart is still needed |
|
|
33
|
+
|
|
34
|
+
## Choose a platform
|
|
35
|
+
|
|
36
|
+
| Requirement | Preferred platform | Inspect first |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| Files, commands, processes, or networking | Host | `fs`, `bash`, `subprocess`, `pty`, and `web` in `Service.listService` |
|
|
39
|
+
| Agents, durable Session data, or Host lifecycle | Host | The relevant Service and `Event.listEvents` |
|
|
40
|
+
| Register a dynamic Tool callable in the next model step | Host | `harness` in `Builtin.listBuiltins`, plus `Tool.listTools` |
|
|
41
|
+
| Page theme, layout, or current page state | Client | `Theme.listTokens` and Client `Service.listService` |
|
|
42
|
+
| Conversation Snapshot or session/workspace lists | Client | The target Slot's standard props and owner props |
|
|
43
|
+
| Settings pages, sidebars, input areas, overlays, or Tool cards | Client | `Slots.listSubTree` |
|
|
44
|
+
| Fetch on Host and display on Client | Both | Host Service + `harness.handle`; Client Slot + `host.call` |
|
|
45
|
+
|
|
46
|
+
Prefer the capability closest to the data owner. If Slot props already provide the Conversation Snapshot, do not fetch it again through Host. If only the Package's own styles need to change, do not override the global theme. If only a small entry point is needed, do not replace an entire product UI region.
|
|
47
|
+
|
|
48
|
+
## Provider navigation
|
|
49
|
+
|
|
50
|
+
Select methods from the actual `cordis_inspect_list` result. Common initial methods include:
|
|
51
|
+
|
|
52
|
+
- `Service.listService`: without `service`, returns every callable Service with its purpose and exact method signatures. Query the selected `service` again for access rules, structured method descriptions/parameters/returns, and only its referenced types.
|
|
53
|
+
- `Event.listEvents`: without `event`, returns every Event with its purpose, dispatch mode, and exact listener signature. Query the selected `event` again for its structured listener contract and only its referenced types; a Waterfall listener must call `next()`.
|
|
54
|
+
- `Builtin.listBuiltins`: returns evaluator-provided symbols and signatures that cannot be obtained through `ctx.get()`.
|
|
55
|
+
- `Slots.listSubTree`: without `root`, returns compact live trees with each Slot's purpose, kind, scope, registration keys, replacement risk, and children. With an exact `root`, it also returns that selected Slot's full contract, props, and current occupants while keeping descendants compact.
|
|
56
|
+
- `Theme.listTokens`: returns theme tokens that may currently be queried and overridden; it does not modify the theme.
|
|
57
|
+
- `Tool.listTools`: returns Tool schemas actually visible to the current Agent, including dynamically registered Tools.
|
|
58
|
+
|
|
59
|
+
Provider names, methods, and inputs must come from the current list result. The Service/Event Catalog describes which interfaces this version permits; it does not guarantee that a Service is currently mounted. At runtime, use real Services and Events rather than caching or displaying Catalog query results.
|
|
60
|
+
|
|
61
|
+
## Execution environment
|
|
62
|
+
|
|
63
|
+
Both `code.host` and `code.client` are plain JavaScript function bodies that return a Cordis Plugin. They are not compiled by TypeScript, JSX, or a bundler.
|
|
64
|
+
|
|
65
|
+
Do not use:
|
|
66
|
+
|
|
67
|
+
- `import`, `require`, TypeScript types, `as`, decorators, or JSX;
|
|
68
|
+
- globals not confirmed by `Builtin.listBuiltins`;
|
|
69
|
+
- guessed access to `window`, `document`, `process`, `Buffer`, `fetch`, or native timers.
|
|
70
|
+
|
|
71
|
+
Client React code must use `React.createElement(...)`.
|
|
72
|
+
|
|
73
|
+
Correct:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
return {
|
|
77
|
+
apply(ctx) {
|
|
78
|
+
const slots = ctx.get('slots')
|
|
79
|
+
if (slots === undefined) return
|
|
80
|
+
slots.inject('tool.view.cordis', () => slots.register(
|
|
81
|
+
{ name: 'tool.view.cordis', key: 'self' },
|
|
82
|
+
() => React.createElement('div', null, 'Hello'),
|
|
83
|
+
))
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Incorrect:
|
|
89
|
+
|
|
90
|
+
```jsx
|
|
91
|
+
return {
|
|
92
|
+
apply(ctx) {
|
|
93
|
+
return <div>Hello</div>
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
JSX is not the only problem in this example. `apply()` registers lifecycle contributions and cannot return a React Element as the Plugin result. UI must be registered in a queried Slot.
|
|
99
|
+
|
|
100
|
+
## Access Services
|
|
101
|
+
|
|
102
|
+
Read optional capabilities with `ctx.get(name)` by default and handle their absence:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
return {
|
|
106
|
+
apply(ctx) {
|
|
107
|
+
const service = ctx.get('serviceName')
|
|
108
|
+
if (service === undefined) return
|
|
109
|
+
service.someMethod()
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Declare `inject` only when a Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
return {
|
|
118
|
+
inject: ['requiredService'],
|
|
119
|
+
apply(ctx) {
|
|
120
|
+
ctx.requiredService.someMethod()
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Do not overuse `inject` merely to avoid an `undefined` check. Do not access `ctx.requiredService` without declaring the injection; the Guard rejects undeclared dependencies.
|
|
126
|
+
|
|
127
|
+
## Manage side effects
|
|
128
|
+
|
|
129
|
+
Every contribution must be removed after the Plugin is stopped, updated, or removed. Prefer Cordis lifecycle APIs:
|
|
130
|
+
|
|
131
|
+
- Use `ctx.on()` to register Event listeners.
|
|
132
|
+
- Use `ctx.effect()` to own an external subscription that returns a disposer.
|
|
133
|
+
- Retain disposers returned by Cordis Service, Tool, Slot, timer, and theme APIs.
|
|
134
|
+
- Do not create process-wide or page-wide side effects at module scope or outside `apply()`.
|
|
135
|
+
|
|
136
|
+
Recommended:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
return {
|
|
140
|
+
apply(ctx) {
|
|
141
|
+
const service = ctx.get('serviceName')
|
|
142
|
+
if (service === undefined) return
|
|
143
|
+
ctx.effect(() => service.subscribe((value) => {
|
|
144
|
+
console.log(value)
|
|
145
|
+
}))
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
If `subscribe()` does not return a disposer, first query whether the Service provides a supported cleanup mechanism. Do not assume unload automatically removes arbitrary third-party callbacks.
|
|
151
|
+
|
|
152
|
+
## Host and Client timers
|
|
153
|
+
|
|
154
|
+
On both platforms, the timer is a Service named `timer` with the same interface; it is not a Builtin. Query `{ "service": "timer" }` through the corresponding platform's `Service.listService` before using it. Declare `inject: ['timer']` before using the timer mixin.
|
|
155
|
+
|
|
156
|
+
One-shot delay:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
return {
|
|
160
|
+
inject: ['timer'],
|
|
161
|
+
apply(ctx) {
|
|
162
|
+
const onClick = () => {
|
|
163
|
+
ctx.timeout(() => console.log('done'), 300)
|
|
164
|
+
}
|
|
165
|
+
// Pass onClick to a queried Slot UI.
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Periodic work in a React component:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
return {
|
|
174
|
+
inject: ['timer'],
|
|
175
|
+
apply(ctx) {
|
|
176
|
+
function Clock() {
|
|
177
|
+
React.useEffect(() => ctx.interval(() => console.log('tick'), 1000), [])
|
|
178
|
+
return React.createElement('div', null, 'Running')
|
|
179
|
+
}
|
|
180
|
+
// Register Clock in a queried Slot.
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Incorrect:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
return {
|
|
189
|
+
apply(ctx) {
|
|
190
|
+
ctx.timeout(() => console.log('invalid'), 300)
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
setTimeout(() => console.log('invalid'), 300)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The first example does not declare the timer hard dependency. The second uses a global timer that does not exist.
|
|
200
|
+
|
|
201
|
+
## Listen to Events
|
|
202
|
+
|
|
203
|
+
Query the Event Provider first to confirm the platform, parameter order, return value, and `mode`.
|
|
204
|
+
|
|
205
|
+
Ordinary emit Event:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
return {
|
|
209
|
+
apply(ctx) {
|
|
210
|
+
ctx.on('some/event', (payload) => {
|
|
211
|
+
console.log(payload)
|
|
212
|
+
})
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The last parameter of a Waterfall Event is `next`. Unless the listener intentionally stops downstream processing, it must call and return it:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
return {
|
|
221
|
+
apply(ctx) {
|
|
222
|
+
ctx.on('some/waterfall', (payload, next) => {
|
|
223
|
+
console.log(payload)
|
|
224
|
+
return next()
|
|
225
|
+
})
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Register Client UI
|
|
231
|
+
|
|
232
|
+
Query `Slots.listSubTree` without `root` to choose a target from the compact purpose and topology tree, then query the exact Slot with `root` before writing its registration. The exact result determines:
|
|
233
|
+
|
|
234
|
+
- the Slot's purpose in the layout;
|
|
235
|
+
- whether its registration protocol is `single`, `list`, `keyed`, or `chain`;
|
|
236
|
+
- registration options;
|
|
237
|
+
- scope standard props and business owner props;
|
|
238
|
+
- current occupants, replacement risks, and descendant Slots.
|
|
239
|
+
|
|
240
|
+
Use `ctx.get('slots')` and handle its absence. Then use `slots.inject` to wait for the Slot declaration and call `slots.register` inside the callback:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
return {
|
|
244
|
+
apply(ctx) {
|
|
245
|
+
const slots = ctx.get('slots')
|
|
246
|
+
if (slots === undefined) return
|
|
247
|
+
slots.inject('target.slot', () => slots.register(
|
|
248
|
+
{ name: 'target.slot', id: 'my-view' },
|
|
249
|
+
(props) => React.createElement('div', null, String(props.someValue)),
|
|
250
|
+
))
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`ctx.get('slots')` does not require an injection. Do not rewrite it as `ctx.slots` unless `inject: ['slots']` is declared:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
return {
|
|
259
|
+
apply(ctx) {
|
|
260
|
+
ctx.slots.register({ name: 'target.slot' }, () => null)
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Do not guess an `id`, `key`, selector, or props before querying the Slot protocol. Do not default to root-level `root`, `sidebar`, `conversation`, or `details` Slots; replacing an entire occupant also removes the descendant Slots it declares.
|
|
266
|
+
|
|
267
|
+
### Settings pages
|
|
268
|
+
|
|
269
|
+
A full settings UI should usually register its own section through `settings.section` to obtain a complete content area. `settings.general.item` is only appropriate for one compact, general-purpose preference. Query the actual subtree, options, and props for both, then select the narrowest entry point that is still sufficient.
|
|
270
|
+
|
|
271
|
+
Dynamic Plugins are temporary and process-local, so their settings UI does not need persistent storage. Do not add durable settings or another persistence mechanism for it. Register the UI in the appropriate settings Slot and keep any transient interaction state in memory for the lifetime of the Plugin.
|
|
272
|
+
|
|
273
|
+
### Session and page data
|
|
274
|
+
|
|
275
|
+
A session-scoped Slot may provide `useSession`, `useSessions`, `useWorkspaces`, `useProjection`, input state, or actions through standard props. Follow the query result and prefer owner or standard props directly; do not add a Host RPC for data already present there.
|
|
276
|
+
|
|
277
|
+
Select only the fields that the UI actually needs. Do not copy or render an entire Conversation Snapshot, Session, Tool call, or Slot props object.
|
|
278
|
+
|
|
279
|
+
### Cordis Run-specific panel
|
|
280
|
+
|
|
281
|
+
To place interactive UI in the latest `cordis_run` card, register `tool.view.cordis` with `key: 'self'`:
|
|
282
|
+
|
|
283
|
+
When the feature needs user interaction tied to this Package's result, this region is often a good fit because it keeps the controls in the conversation flow beside the Run card. It is not the default target for every Client UI: settings, sidebars, message actions, and overlays should use their own queried Slots when those locations better match the feature.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
return {
|
|
287
|
+
apply(ctx) {
|
|
288
|
+
const slots = ctx.get('slots')
|
|
289
|
+
if (slots === undefined) return
|
|
290
|
+
slots.inject('tool.view.cordis', () => slots.register(
|
|
291
|
+
{ name: 'tool.view.cordis', key: 'self' },
|
|
292
|
+
(props) => React.createElement('div', null, `Package ${props.packageId}`),
|
|
293
|
+
))
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
At runtime, `self` binds to `pluginId + packageId`. Do not include `pluginRunId` in the key. When the same Package runs multiple times, the latest Run card hosts the UI and older cards automatically degrade.
|
|
299
|
+
|
|
300
|
+
### Ordinary Tool cards
|
|
301
|
+
|
|
302
|
+
To customize the call card for an ordinary model Tool, query `tool.call.toolview`. Its key is the Tool name; registering an existing key may replace the product's default card. When customizing only a newly added Tool, first verify its schema with `Tool.listTools`, then query the complete `ToolCallOwnerProps`.
|
|
303
|
+
|
|
304
|
+
### Overlays and local entry points
|
|
305
|
+
|
|
306
|
+
- For toasts, status notices, and frame-wide overlays, query `shell.overlay` first; observe its pointer-events and ordering rules.
|
|
307
|
+
- When the selected target is a global overlay Slot, decide whether the UI should be draggable, how the user shows and hides it, and which existing layers it must cover or remain below.
|
|
308
|
+
- For small sidebar actions, prefer additive inner Slots such as `sidebar.footer.action`; do not replace the entire sidebar.
|
|
309
|
+
- For supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules.
|
|
310
|
+
|
|
311
|
+
## Themes and styles
|
|
312
|
+
|
|
313
|
+
Determine the scope of the change first:
|
|
314
|
+
|
|
315
|
+
1. Global theme: first query `Theme.listTokens`, then query `{ "service": "theme" }` through Client `Service.listService`. Supply light and dark values for each override as required by the query, and retain the returned disposer.
|
|
316
|
+
2. The Package's own components: use `styles.insert(css)` and prefer theme CSS variables for colors.
|
|
317
|
+
3. New visible content: choose a Slot first, then decide between local CSS and global tokens.
|
|
318
|
+
|
|
319
|
+
Do not manipulate `document.body`, `window`, or hard-coded product DOM selectors. The theme Service changes tokens but does not create UI. Slots create UI but do not replace the theme system.
|
|
320
|
+
|
|
321
|
+
## Call Host from Client
|
|
322
|
+
|
|
323
|
+
Host registers a Package-private method with `harness.handle(method, handler)`, and Client invokes it with `host.call(method, args)`. This is Client→Host JSON RPC.
|
|
324
|
+
|
|
325
|
+
Host:
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
return {
|
|
329
|
+
apply(ctx) {
|
|
330
|
+
harness.handle('read-state', async (args) => {
|
|
331
|
+
return { value: args.key }
|
|
332
|
+
})
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Client:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
return {
|
|
341
|
+
async apply(ctx) {
|
|
342
|
+
const result = await host.call('read-state', { key: 'demo' })
|
|
343
|
+
console.log(result.value)
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Arguments and return values must be lossless JSON. Do not pass functions, React elements, class instances, Contexts, Services, or other runtime objects; return `null` when there is no response data. Do not register a public Remote Service or use `ctx.remote` for Package-private communication.
|
|
349
|
+
|
|
350
|
+
## Register a dynamic model Tool
|
|
351
|
+
|
|
352
|
+
Host can use `harness` to register a Tool callable in the next model step. First query the current `harness` signature with Host `Builtin.listBuiltins`, then inspect existing Tool names and schemas with `Tool.listTools` to avoid conflicts.
|
|
353
|
+
|
|
354
|
+
Tool arguments and return values must be JSON-compatible. `execute` owns the business result; render and presentation own only what the model and native UI see. Tool registration must belong to the current Plugin Fiber so it is automatically removed after stop or update.
|
|
355
|
+
|
|
356
|
+
## Handle internal live data
|
|
357
|
+
|
|
358
|
+
Service instances, Event payloads, Slot props, Session and Conversation Snapshots, Tool state, and other DSH/Cordis objects are internal live data.
|
|
359
|
+
|
|
360
|
+
Do not:
|
|
361
|
+
|
|
362
|
+
- call `JSON.stringify` or `structuredClone` on these objects or their descendants;
|
|
363
|
+
- recursively enumerate, fully copy, or display them as a whole;
|
|
364
|
+
- place Host objects in the Package's long-lived state or RPC return values.
|
|
365
|
+
|
|
366
|
+
Read only the leaf fields required by the current feature. Extract the minimum strings, numbers, booleans, and other scalar values before constructing owned JSON.
|
|
367
|
+
|
|
368
|
+
## Versions, approval, and repair
|
|
369
|
+
|
|
370
|
+
- A Plugin is the stable instance identified by `pluginId`.
|
|
371
|
+
- A Package is an immutable code version identified by `packageId`.
|
|
372
|
+
- Every activation attempt has its own `pluginRunId`.
|
|
373
|
+
- `currentPackageId` is the latest successful version; it does not imply that the Plugin is currently running.
|
|
374
|
+
- `nextPackageId` is the target awaiting approval, activating, awaiting Client activation, or most recently failed.
|
|
375
|
+
|
|
376
|
+
Choose the `cordis_run` mode as follows:
|
|
377
|
+
|
|
378
|
+
| Current state | Target | mode |
|
|
379
|
+
| --- | --- | --- |
|
|
380
|
+
| No current | Any Package under the Plugin | `run` |
|
|
381
|
+
| Has current | The same Package | `run` |
|
|
382
|
+
| Has current | A different Package | `update` |
|
|
383
|
+
| Update failed | `nextPackageId` | `update` to retry |
|
|
384
|
+
| Update failed | `currentPackageId` | `run` to roll back |
|
|
385
|
+
|
|
386
|
+
An unauthorized Client Package returns `awaiting-approval`. A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains after a technical runtime failure. An authorized Package returns `starting` and completes asynchronously in the browser.
|
|
387
|
+
|
|
388
|
+
After a technical failure:
|
|
389
|
+
|
|
390
|
+
1. Use `cordis_inspect_self(pluginId, packageId)` to read the failed version's source and exact diagnostics.
|
|
391
|
+
2. If the error involves an unknown capability, list and query the corresponding Provider again.
|
|
392
|
+
3. Define a new Package under the same Plugin; do not overwrite the failed Package.
|
|
393
|
+
4. Run again with the new `packageId` and the correct mode.
|
|
394
|
+
|
|
395
|
+
Do not retry automatically after the user rejects approval. A failed update does not automatically restore the old physical Run; explicitly run current when recovery is required.
|
|
396
|
+
|
|
397
|
+
## Modify @pluginId
|
|
398
|
+
|
|
399
|
+
When the user identifies a target with `@pluginId`, do not create another Plugin. The injected context contains only identity, version pointers, and the default base Package, not source code.
|
|
400
|
+
|
|
401
|
+
Modify it as follows:
|
|
402
|
+
|
|
403
|
+
1. Read the base Package with `cordis_inspect_self(pluginId, packageId)`.
|
|
404
|
+
2. Preserve the Host or Client half that does not need to change and modify only the target code.
|
|
405
|
+
3. Call `cordis_define` with `plugin.kind: 'existing'` and the original `pluginId`.
|
|
406
|
+
4. Use the returned `packageId`; when current exists, activate the new version with `update` in the usual case.
|
|
407
|
+
|
|
408
|
+
If the reference is unavailable, explain that the Plugin was removed, belongs to another Session, or was lost on process restart. Do not create a same-named replacement.
|
|
409
|
+
|
|
410
|
+
## Common failure checks
|
|
411
|
+
|
|
412
|
+
| Failure | Check first |
|
|
413
|
+
| --- | --- |
|
|
414
|
+
| `service "x" is not declared` | Whether code uses `ctx.x` without declaring `inject: ['x']` on the Plugin object; switch to `ctx.get('x')` with an absence check or declare a true hard dependency |
|
|
415
|
+
| `cannot get property "timer" without inject` | Query the timer Service and declare `inject: ['timer']` |
|
|
416
|
+
| Client parse failure | Whether the code uses JSX, TypeScript, import, or an unavailable global |
|
|
417
|
+
| Slot registration failure | Whether the live subtree was queried, the Slot exists, and options, key, or selector satisfy the returned protocol |
|
|
418
|
+
| UI loads but the page reports an error | Inspect the `client-render` diagnostic and stack; the error belongs to an exact Run, so define a new Package to repair it |
|
|
419
|
+
| `host.call` failure | The Host handler name, current `pluginRunId`, JSON arguments, and real Service dependencies inside the handler |
|
|
420
|
+
| Update failure | Preserve current/next semantics; repair next and update, or run current to roll back |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: editing-cordis-compositions
|
|
3
|
+
description: Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Editing Cordis compositions
|
|
7
|
+
|
|
8
|
+
Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.
|
|
9
|
+
|
|
10
|
+
## Off-limits
|
|
11
|
+
|
|
12
|
+
**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.
|
|
13
|
+
|
|
14
|
+
To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.
|
|
15
|
+
|
|
16
|
+
## Decide the plane first
|
|
17
|
+
|
|
18
|
+
Two planes, and the choice is not about how "agent-related" something feels — it is about whether the thing must be shared.
|
|
19
|
+
|
|
20
|
+
**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.
|
|
21
|
+
|
|
22
|
+
**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.
|
|
23
|
+
|
|
24
|
+
**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.
|
|
25
|
+
|
|
26
|
+
A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.
|
|
27
|
+
|
|
28
|
+
Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.
|
|
29
|
+
|
|
30
|
+
## The roster service
|
|
31
|
+
|
|
32
|
+
`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.
|
|
33
|
+
|
|
34
|
+
Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. What this skill relies on:
|
|
35
|
+
|
|
36
|
+
- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.
|
|
37
|
+
- `read(id)` — one preset's composition text, without a file tool or a path.
|
|
38
|
+
- `copy(from, id, name?)` — the only authoring write (see below).
|
|
39
|
+
- `standingKeyFor(id)` — mount-validate one preset (see below).
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
return {
|
|
43
|
+
name: 'preset-tools',
|
|
44
|
+
inject: ['agentPresets', 'tools'],
|
|
45
|
+
apply(ctx) {
|
|
46
|
+
harness.registerTool(ctx, harness.defineTool({
|
|
47
|
+
name: 'preset_check',
|
|
48
|
+
description: 'Mount-validate one preset by id.',
|
|
49
|
+
parameters: { id: { type: 'string', required: true } },
|
|
50
|
+
output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },
|
|
51
|
+
async execute(args) {
|
|
52
|
+
try {
|
|
53
|
+
await ctx.agentPresets.standingKeyFor(args.id)
|
|
54
|
+
return 'mounted OK'
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return error.message
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}))
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.
|
|
65
|
+
|
|
66
|
+
## Authoring a preset
|
|
67
|
+
|
|
68
|
+
1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.
|
|
69
|
+
2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.
|
|
70
|
+
3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.
|
|
71
|
+
4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.
|
|
72
|
+
5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.
|
|
73
|
+
|
|
74
|
+
A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.
|
|
75
|
+
|
|
76
|
+
## The rule that catches people
|
|
77
|
+
|
|
78
|
+
**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.
|
|
79
|
+
|
|
80
|
+
Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:"services"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.
|
|
81
|
+
|
|
82
|
+
When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
- id: delegation
|
|
86
|
+
name: cordis:group
|
|
87
|
+
group: true
|
|
88
|
+
isolate:
|
|
89
|
+
workflows: true
|
|
90
|
+
config:
|
|
91
|
+
- id: workflow-worker-thread
|
|
92
|
+
name: '@monotykamary/dsh-workflow-worker-thread'
|
|
93
|
+
config:
|
|
94
|
+
provider: spawn
|
|
95
|
+
- id: tool-workflow
|
|
96
|
+
name: '@monotykamary/dsh-tool-workflow'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.
|
|
100
|
+
|
|
101
|
+
A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.
|
|
102
|
+
|
|
103
|
+
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
|
|
104
|
+
|
|
105
|
+
## Verifying a change
|
|
106
|
+
|
|
107
|
+
**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:
|
|
108
|
+
|
|
109
|
+
- a row whose package does not resolve (`Cannot find package …`);
|
|
110
|
+
- a row whose config is invalid (`invalid config: $.<field> missing required value`);
|
|
111
|
+
- a row that never activated (`N row(s) did not activate: <id>: waiting for <service>`);
|
|
112
|
+
- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) [<name>]; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service "<name>" has been registered at <Owner>`. Both name the offending service.
|
|
113
|
+
|
|
114
|
+
It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.
|
|
115
|
+
|
|
116
|
+
**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.
|
|
117
|
+
|
|
118
|
+
`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.
|
|
119
|
+
|
|
120
|
+
After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.
|
|
121
|
+
|
|
122
|
+
`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.
|
|
123
|
+
|
|
124
|
+
## Native product subagents
|
|
125
|
+
|
|
126
|
+
Codex and Claude Code providers already live in the host composition. A preset chooses either product by contributing the same ordinary delegation-tool row used for spawn and fork; never move a product provider into the preset and never add a product-specific settings field.
|
|
127
|
+
|
|
128
|
+
Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
- id: tool-subagent-codex
|
|
132
|
+
name: '@monotykamary/dsh-tool-subagent'
|
|
133
|
+
disabled: true
|
|
134
|
+
config:
|
|
135
|
+
provider: codex
|
|
136
|
+
toolName: subagent_codex
|
|
137
|
+
enableRunInBackground: false
|
|
138
|
+
maxDepth: provider-managed
|
|
139
|
+
|
|
140
|
+
- id: tool-subagent-claude-code
|
|
141
|
+
name: '@monotykamary/dsh-tool-subagent'
|
|
142
|
+
disabled: true
|
|
143
|
+
config:
|
|
144
|
+
provider: claude-code
|
|
145
|
+
toolName: subagent_claude_code
|
|
146
|
+
enableRunInBackground: false
|
|
147
|
+
maxDepth: provider-managed
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
|
|
151
|
+
|
|
152
|
+
## What not to move into a preset
|
|
153
|
+
|
|
154
|
+
`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition.
|
|
2
|
+
#
|
|
3
|
+
# The persona is the complete system prompt, so global identity, Web orientation,
|
|
4
|
+
# tool guidance, and later assembly listeners cannot add prompt text. Runtime
|
|
5
|
+
# context snapshots are suppressed for this preset, and the model composes only
|
|
6
|
+
# persistent `bash` and `str_replace_editor`. Context compaction is absent.
|
|
7
|
+
|
|
8
|
+
- id: persona
|
|
9
|
+
name: '@monotykamary/dsh-persona'
|
|
10
|
+
config:
|
|
11
|
+
text: You are a helpful software engineer assistant.
|
|
12
|
+
complete: true
|
|
13
|
+
includeRuntimeContext: false
|
|
14
|
+
|
|
15
|
+
# The PTY registry is an agent-owned service, so it lives in an entry-local
|
|
16
|
+
# realm. The backend still consumes the host sandbox policy and subprocess
|
|
17
|
+
# implementation, while the tool registers into this agent's scoped catalog.
|
|
18
|
+
- id: persistent-shell
|
|
19
|
+
name: cordis:group
|
|
20
|
+
group: true
|
|
21
|
+
isolate:
|
|
22
|
+
terminals: true
|
|
23
|
+
config:
|
|
24
|
+
- id: pty
|
|
25
|
+
name: '@monotykamary/dsh-terminal'
|
|
26
|
+
|
|
27
|
+
- id: terminal-bash
|
|
28
|
+
name: '@monotykamary/dsh-terminal-bash'
|
|
29
|
+
config:
|
|
30
|
+
timeoutMs: 300000
|
|
31
|
+
|
|
32
|
+
- id: persistent-bash
|
|
33
|
+
name: '@monotykamary/dsh-tool-bash-persistent'
|
|
34
|
+
config:
|
|
35
|
+
timeoutMs: 300000
|
|
36
|
+
description: |-
|
|
37
|
+
Run commands in a bash shell
|
|
38
|
+
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
|
39
|
+
* You don't have access to the internet via this tool.
|
|
40
|
+
* You do have access to a mirror of common linux and python packages via apt and pip.
|
|
41
|
+
* State is persistent across command calls and discussions with the user.
|
|
42
|
+
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
|
|
43
|
+
* Please avoid commands that may produce a very large amount of output.
|
|
44
|
+
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
|
|
45
|
+
|
|
46
|
+
# The bare local filesystem shadows the host's sandboxed provider only for this
|
|
47
|
+
# preset. The editor shares that realm and requires absolute paths.
|
|
48
|
+
- id: filesystem
|
|
49
|
+
name: cordis:group
|
|
50
|
+
group: true
|
|
51
|
+
isolate:
|
|
52
|
+
fs: true
|
|
53
|
+
config:
|
|
54
|
+
- id: fs-local
|
|
55
|
+
name: '@monotykamary/dsh-fs-local'
|
|
56
|
+
config:
|
|
57
|
+
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
|
58
|
+
|
|
59
|
+
- id: str-replace-editor
|
|
60
|
+
name: '@monotykamary/dsh-tool-str-replace-editor'
|
|
61
|
+
config:
|
|
62
|
+
maxOutputChars: 16000
|