@elinpf/dsh-ops-shell-tool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +4 -0
- package/README.md +32 -0
- package/README.zh.md +32 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.js +188 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +25 -0
- package/lib/types.d.ts +74 -0
- package/lib/types.js +9 -0
- package/package.json +52 -0
package/README.i18n.yaml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @elinpf/dsh-ops-shell-tool
|
|
2
|
+
|
|
3
|
+
Shared factory for ops-access consumer tools: the standard shell result shape `{ exitCode, stdout, stderr, command, error? }`, its output schema and render, and the resolve-per-call execute template.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
A pure library (not a plugin) — the single home for the boilerplate every ops command tool (`ops-tool-kubectl`, `ops-tool-ceph`, `ops-tool-ssh`) would otherwise duplicate. A consumer calls `registerProfiledShellTool(ctx, spec)` from its own plugin and keeps only its identity: tool name, the ops-access kind it resolves, the profile-arg name, and `buildCommand`.
|
|
8
|
+
|
|
9
|
+
- **Standard result shape** — one `ShellToolResult` definition, one output schema, one pure render, shared by every consumer tool.
|
|
10
|
+
- **Resolve per call** — the profile is resolved through `ctx.get('opsAccess')` inside execute, never statically injected, never cached.
|
|
11
|
+
- **Credential tokens** — `buildCommand` marks file-bearing fields with `ref(field)`, which mints a display token `<id@tier:field>`; the executed command carries the shell-quoted real value, while the displayed command and all captured stdout/stderr are scrubbed back to tokens. Credential paths never reach the model or the session event log.
|
|
12
|
+
- **Honest kill reporting** — 30 s default timeout; a signal death normalizes to `exitCode: -1` with the cause (timeout / caller abort / signal name) spelled out in the `error` field, never a bare -1.
|
|
13
|
+
- **stderr noise filtering** — consumer-declared regexes drop known-noise stderr lines (e.g. ceph keyring chatter) after scrubbing.
|
|
14
|
+
- **`shellQuote`** — exported for consumers that must embed a whole remote command as one argument (ops-tool-ssh).
|
|
15
|
+
|
|
16
|
+
## Design notes
|
|
17
|
+
|
|
18
|
+
- **Why a factory, not a base plugin:** the three consumer tools differ only in identity pieces. Owning the execute template here means timeout handling, kill notes, and credential scrubbing are fixed once, not three times.
|
|
19
|
+
- **Why `ctx.get` per call instead of a static inject:** the preset mounts the group concurrently, so a static `inject: ['opsAccess']` risks a loader deadlock against the definition row; by tool-call time the service is long since provided. Same discipline as the registry file itself: resolve per operation, cache nothing.
|
|
20
|
+
- **Why per-call tokens:** token → value mappings live only inside one execute, so a credential path can never leak across calls or sessions.
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
None — the package has no plugin `Config`. All behavior is parameterized per consumer through `ProfiledShellToolSpec`: `name`, `kind`, `targetParam`, the three description strings, `buildCommand`, `timeoutMs` (default 30000), and `stderrNoise`.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx vitest run
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The spec drives the factory through a mock ctx: resolve-per-call, agent passthrough, the absent-seam guard, error passthrough, exitCode normalization, timeout/abort/signal kill notes, credential token substitution and scrubbing, stderr noise filtering, render purity, and HMR unload (the registered tool disappears when the fiber's effect disposers run).
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @elinpf/dsh-ops-shell-tool
|
|
2
|
+
|
|
3
|
+
ops-access 消费方工具共享的工厂:统一的 shell 结果形状 `{ exitCode, stdout, stderr, command, error? }`、输出 schema 与 render、以及按调用解析(resolve-per-call)的 execute 模板。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
纯库(不是插件)——把每个 ops 命令工具(`ops-tool-kubectl`、`ops-tool-ceph`、`ops-tool-ssh`)否则会各自重复的样板收敛到一处。消费方在自己的插件里调用 `registerProfiledShellTool(ctx, spec)`,只保留身份四件套:工具名、解析的 ops-access kind、profile 参数名、`buildCommand`。
|
|
8
|
+
|
|
9
|
+
- **统一结果形状** — 一份 `ShellToolResult` 定义、一份输出 schema、一份纯 render,所有消费方工具共用。
|
|
10
|
+
- **按调用解析** — profile 在 execute 内通过 `ctx.get('opsAccess')` 解析,不做静态 inject,不缓存。
|
|
11
|
+
- **凭据 token** — `buildCommand` 用 `ref(field)` 标记含文件的字段,生成展示 token `<id@tier:field>`;实际执行的命令携带 shell 转义后的真实值,而展示命令和捕获的 stdout/stderr 全部洗回 token。凭据路径永远不进入模型上下文或会话事件日志。
|
|
12
|
+
- **诚实的 kill 报告** — 默认 30s 超时;信号死亡归一为 `exitCode: -1`,并在 `error` 字段写明原因(超时 / 调用方取消 / 信号名),绝不留一个光秃秃的 -1。
|
|
13
|
+
- **stderr 噪音过滤** — 消费方声明的正则在清洗之后丢弃已知噪音行(如 ceph keyring 唠叨)。
|
|
14
|
+
- **`shellQuote`** — 导出给需要把整条远端命令作为单个参数嵌入的消费方(ops-tool-ssh)。
|
|
15
|
+
|
|
16
|
+
## 设计要点
|
|
17
|
+
|
|
18
|
+
- **为什么是工厂而不是基类插件:** 三个消费方工具只差身份件。execute 模板收在这里,超时处理、kill 说明、凭据清洗只修一处,而不是三处。
|
|
19
|
+
- **为什么用 `ctx.get` 按调用解析而不是静态 inject:** preset 并发挂载同组插件,静态 `inject: ['opsAccess']` 有对定义行死锁的风险;到工具调用时服务早已就绪。与 registry 文件同一纪律:按操作解析,什么都不缓存。
|
|
20
|
+
- **为什么 token 按调用生成:** token → 真实值的映射只存活于单次 execute 内,凭据路径不可能跨调用、跨会话泄漏。
|
|
21
|
+
|
|
22
|
+
## 配置项
|
|
23
|
+
|
|
24
|
+
无 —— 本包没有插件 `Config`。所有行为由消费方通过 `ProfiledShellToolSpec` 参数化:`name`、`kind`、`targetParam`、三段描述文案、`buildCommand`、`timeoutMs`(默认 30000)、`stderrNoise`。
|
|
25
|
+
|
|
26
|
+
## 测试方式
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx vitest run
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
测试用 mock ctx 驱动工厂:按调用解析、agent 透传、seam 缺失守卫、错误透传、exitCode 归一、超时/中止/信号三类 kill 说明、凭据 token 替换与清洗、stderr 噪音过滤、render 纯函数性,以及 HMR 卸载(fiber 的 effect disposer 执行后注册的工具随之消失)。
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ops shell tool factory.
|
|
3
|
+
*
|
|
4
|
+
* One home for the boilerplate every ops-access consumer tool shares:
|
|
5
|
+
* the result shape `{ exitCode, stdout, stderr, command, error? }`, its
|
|
6
|
+
* output schema, the render function, and the execute template (resolve the
|
|
7
|
+
* profile per call through `ctx.get('opsAccess')` — never a static inject,
|
|
8
|
+
* never cached — assemble the command, run it through `ctx.shell` with a
|
|
9
|
+
* fixed 30s timeout, normalize a signal-killed exitCode to -1 while
|
|
10
|
+
* surfacing the kill cause (timeout / caller abort / signal name) in the
|
|
11
|
+
* error field, pass errors through verbatim).
|
|
12
|
+
*
|
|
13
|
+
* Credential paths never reach the model or the event log. Consumers mark
|
|
14
|
+
* file-bearing fields with the `ref()` helper, which emits a display token
|
|
15
|
+
* `<id@tier:field>`; the factory substitutes the shell-quoted real value
|
|
16
|
+
* only in the command handed to `ctx.shell`, and scrubs every occurrence
|
|
17
|
+
* of a referenced value back to its token in the displayed command AND in
|
|
18
|
+
* captured stdout/stderr (CLIs like kubectl print their --kubeconfig path
|
|
19
|
+
* in error output). The model sees `kubectl --kubeconfig=<prod@rw:kubeconfigPath>`,
|
|
20
|
+
* never `/root/.dsh-ops/credentials/...`.
|
|
21
|
+
*
|
|
22
|
+
* A consumer package keeps only its identity: tool name, the kind it
|
|
23
|
+
* resolves, how to assemble the command from profile fields, and optionally
|
|
24
|
+
* `stderrNoise` — regexes for known-noise stderr lines (e.g. ceph's
|
|
25
|
+
* missing-default-keyring chatter) the factory drops after scrubbing.
|
|
26
|
+
*
|
|
27
|
+
* @module @elinpf/dsh-ops-shell-tool
|
|
28
|
+
*/
|
|
29
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
30
|
+
import type { ProfiledShellToolSpec } from './types.js';
|
|
31
|
+
export type { CredentialRef, ProfiledShellToolSpec, ShellToolExec, ShellToolResult } from './types.js';
|
|
32
|
+
/**
|
|
33
|
+
* Single-quote a value for safe shell embedding. Used for ref-token
|
|
34
|
+
* substitutes, and exported for consumers that must pass a whole remote
|
|
35
|
+
* command as ONE argument (ops-tool-ssh).
|
|
36
|
+
*/
|
|
37
|
+
export declare function shellQuote(value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Register a profiled shell tool on `ctx.tools`, disposed with the plugin's
|
|
40
|
+
* fiber. The caller's plugin must declare `inject = ['shell', 'tools']`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function registerProfiledShellTool(ctx: Context, spec: ProfiledShellToolSpec): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ops shell tool factory.
|
|
3
|
+
*
|
|
4
|
+
* One home for the boilerplate every ops-access consumer tool shares:
|
|
5
|
+
* the result shape `{ exitCode, stdout, stderr, command, error? }`, its
|
|
6
|
+
* output schema, the render function, and the execute template (resolve the
|
|
7
|
+
* profile per call through `ctx.get('opsAccess')` — never a static inject,
|
|
8
|
+
* never cached — assemble the command, run it through `ctx.shell` with a
|
|
9
|
+
* fixed 30s timeout, normalize a signal-killed exitCode to -1 while
|
|
10
|
+
* surfacing the kill cause (timeout / caller abort / signal name) in the
|
|
11
|
+
* error field, pass errors through verbatim).
|
|
12
|
+
*
|
|
13
|
+
* Credential paths never reach the model or the event log. Consumers mark
|
|
14
|
+
* file-bearing fields with the `ref()` helper, which emits a display token
|
|
15
|
+
* `<id@tier:field>`; the factory substitutes the shell-quoted real value
|
|
16
|
+
* only in the command handed to `ctx.shell`, and scrubs every occurrence
|
|
17
|
+
* of a referenced value back to its token in the displayed command AND in
|
|
18
|
+
* captured stdout/stderr (CLIs like kubectl print their --kubeconfig path
|
|
19
|
+
* in error output). The model sees `kubectl --kubeconfig=<prod@rw:kubeconfigPath>`,
|
|
20
|
+
* never `/root/.dsh-ops/credentials/...`.
|
|
21
|
+
*
|
|
22
|
+
* A consumer package keeps only its identity: tool name, the kind it
|
|
23
|
+
* resolves, how to assemble the command from profile fields, and optionally
|
|
24
|
+
* `stderrNoise` — regexes for known-noise stderr lines (e.g. ceph's
|
|
25
|
+
* missing-default-keyring chatter) the factory drops after scrubbing.
|
|
26
|
+
*
|
|
27
|
+
* @module @elinpf/dsh-ops-shell-tool
|
|
28
|
+
*/
|
|
29
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
function errorMessage(e) {
|
|
32
|
+
return String(e?.message || e);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Single-quote a value for safe shell embedding. Used for ref-token
|
|
36
|
+
* substitutes, and exported for consumers that must pass a whole remote
|
|
37
|
+
* command as ONE argument (ops-tool-ssh).
|
|
38
|
+
*/
|
|
39
|
+
export function shellQuote(value) {
|
|
40
|
+
return "'" + value.split("'").join("'\\''") + "'";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Credential tokens minted per tool call: display token `<id@tier:field>` →
|
|
44
|
+
* raw field value. The display command keeps tokens; the executed command
|
|
45
|
+
* substitutes quoted values; captured output is scrubbed value → token so
|
|
46
|
+
* credential paths never reach the model or the session event log — one
|
|
47
|
+
* mechanism, shared by every consumer tool.
|
|
48
|
+
*/
|
|
49
|
+
function createCredentialTokens(profileName, tier, fields) {
|
|
50
|
+
const secrets = new Map();
|
|
51
|
+
const ref = (field) => {
|
|
52
|
+
const value = fields[field];
|
|
53
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
54
|
+
throw new Error('ops-access: profile ' + profileName + ' field "' + field + '" is not a non-empty string — ref() is for credential file fields');
|
|
55
|
+
}
|
|
56
|
+
const token = '<' + profileName + '@' + tier + ':' + field + '>';
|
|
57
|
+
secrets.set(token, value);
|
|
58
|
+
return token;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
ref,
|
|
62
|
+
executable(displayCommand) {
|
|
63
|
+
let out = displayCommand;
|
|
64
|
+
for (const [token, value] of secrets) {
|
|
65
|
+
out = out.split(token).join(shellQuote(value));
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
},
|
|
69
|
+
scrub(text) {
|
|
70
|
+
let out = text;
|
|
71
|
+
for (const [token, value] of secrets) {
|
|
72
|
+
if (value.length >= 8)
|
|
73
|
+
out = out.split(value).join(token);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Drop stderr lines matching any consumer-declared noise pattern. Line-based:
|
|
81
|
+
* a trailing newline survives filtering (the final empty segment is not a
|
|
82
|
+
* line), and stdout is never touched.
|
|
83
|
+
*/
|
|
84
|
+
function dropNoiseLines(text, patterns) {
|
|
85
|
+
if (!patterns?.length || !text)
|
|
86
|
+
return text;
|
|
87
|
+
return text.split('\n').filter((line) => !patterns.some((p) => p.test(line))).join('\n');
|
|
88
|
+
}
|
|
89
|
+
/** The shared output contract: schema + render, both pure. */
|
|
90
|
+
const output = {
|
|
91
|
+
schema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
properties: {
|
|
95
|
+
exitCode: { type: 'number', required: true },
|
|
96
|
+
stdout: { type: 'string', required: true },
|
|
97
|
+
stderr: { type: 'string', required: true },
|
|
98
|
+
command: { type: 'string', required: true },
|
|
99
|
+
error: { type: 'string' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// Pure function of (args, value): same inputs, same text, no state touched.
|
|
103
|
+
render: (_args, value) => {
|
|
104
|
+
const parts = [];
|
|
105
|
+
if (value.command)
|
|
106
|
+
parts.push(`$ ${value.command}`);
|
|
107
|
+
if (value.stdout)
|
|
108
|
+
parts.push(value.stdout);
|
|
109
|
+
if (value.stderr)
|
|
110
|
+
parts.push(`[stderr]\n${value.stderr}`);
|
|
111
|
+
if (value.error)
|
|
112
|
+
parts.push(`[error] ${value.error}`);
|
|
113
|
+
if (value.exitCode !== 0 && value.exitCode !== undefined && value.exitCode !== null) {
|
|
114
|
+
parts.push(`[exit code: ${value.exitCode}]`);
|
|
115
|
+
}
|
|
116
|
+
return [{ type: 'text', text: parts.join('\n\n') || '(no output)' }];
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
// ── Factory ──────────────────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Register a profiled shell tool on `ctx.tools`, disposed with the plugin's
|
|
122
|
+
* fiber. The caller's plugin must declare `inject = ['shell', 'tools']`.
|
|
123
|
+
*/
|
|
124
|
+
export function registerProfiledShellTool(ctx, spec) {
|
|
125
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
126
|
+
name: spec.name,
|
|
127
|
+
description: spec.description,
|
|
128
|
+
parameters: {
|
|
129
|
+
[spec.targetParam]: { type: 'string', required: true, description: spec.targetParamDescription },
|
|
130
|
+
command: { type: 'string', required: true, description: spec.commandDescription },
|
|
131
|
+
},
|
|
132
|
+
output,
|
|
133
|
+
async execute(args, exec) {
|
|
134
|
+
let fullCommand = '';
|
|
135
|
+
try {
|
|
136
|
+
// Resolve the seam per call through ctx.get: the preset mounts the
|
|
137
|
+
// group concurrently, so 'opsAccess' must not be a static inject
|
|
138
|
+
// (deadlock risk against the definition row), and by tool-call time
|
|
139
|
+
// the service is long since provided. Same discipline as the
|
|
140
|
+
// registry file itself: resolve per operation, cache nothing.
|
|
141
|
+
const opsAccess = ctx.get('opsAccess');
|
|
142
|
+
if (!opsAccess) {
|
|
143
|
+
const message = 'ops-access service unavailable — is the ops-access plugin mounted in this preset?';
|
|
144
|
+
return { error: message, exitCode: -1, stdout: '', stderr: message, command: '' };
|
|
145
|
+
}
|
|
146
|
+
// Pass the caller agent through so the access gate (if mounted) can
|
|
147
|
+
// key grants on the session id. Without a gate this arg is inert.
|
|
148
|
+
const profile = await opsAccess.resolve(spec.kind, args[spec.targetParam], exec.agent);
|
|
149
|
+
// Mint per-call credential tokens: buildCommand marks file fields via
|
|
150
|
+
// ref(); the display command (model-visible, logged) keeps the tokens,
|
|
151
|
+
// only the executed command carries the real values.
|
|
152
|
+
const tokens = createCredentialTokens(profile.name, profile.tier, profile.fields);
|
|
153
|
+
fullCommand = tokens.scrub(spec.buildCommand(profile.fields, args.command, tokens.ref));
|
|
154
|
+
const request = { command: tokens.executable(fullCommand), timeoutMs: spec.timeoutMs ?? 30000, signal: exec.signal };
|
|
155
|
+
const resolved = ctx.shell.resolve(request);
|
|
156
|
+
const result = await ctx.shell.run(resolved);
|
|
157
|
+
// exitCode is null when the process died from a signal — normalize to
|
|
158
|
+
// -1, and the kill's cause is always surfaced in the error field so
|
|
159
|
+
// the model never has to guess WHY there is no exit code — a bare -1
|
|
160
|
+
// used to leave it guessing, and it guessed wrong ('unstable pipes'
|
|
161
|
+
// for a wedged remote end, 2026-08-27).
|
|
162
|
+
// stdout/stderr are scrubbed value → token: CLIs echo credential
|
|
163
|
+
// paths in errors, and the event log must never see them. stderr then
|
|
164
|
+
// loses the consumer-declared noise lines (e.g. ceph keyring chatter).
|
|
165
|
+
const killNote = result.timedOut
|
|
166
|
+
? 'killed: exceeded the ' + Math.round(result.timeoutMs / 1000) + 's tool timeout — the command never finished and may have partially taken effect (e.g. a delete was already issued). If the remote end may be hung, give the remote command its own timeout (e.g. wget -T N, timeout Ns); for slow batch operations use a non-waiting form (e.g. kubectl delete --wait=false).'
|
|
167
|
+
: result.aborted
|
|
168
|
+
? 'aborted: the caller cancelled this run before it finished.'
|
|
169
|
+
: result.exitCode === null
|
|
170
|
+
? 'killed by signal ' + (result.signal ?? 'unknown') + ' — no normal exit code; the process was terminated externally (OOM killer, sandbox policy, or a deliberate kill).'
|
|
171
|
+
: undefined;
|
|
172
|
+
return {
|
|
173
|
+
exitCode: result.exitCode ?? -1,
|
|
174
|
+
stdout: tokens.scrub(result.stdout.text),
|
|
175
|
+
stderr: dropNoiseLines(tokens.scrub(result.stderr.text), spec.stderrNoise),
|
|
176
|
+
command: fullCommand,
|
|
177
|
+
...(killNote !== undefined ? { error: killNote } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
// Unknown profile names land here too — resolve's message already
|
|
182
|
+
// lists the available names, so pass it through verbatim.
|
|
183
|
+
const message = errorMessage(e);
|
|
184
|
+
return { error: message, exitCode: -1, stdout: '', stderr: message, command: fullCommand };
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
})));
|
|
188
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-shell-tool.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-shell-tool/invariant
|
|
5
|
+
*/
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
declare const name = "ops-shell-tool-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Context carrying the invariant service.
|
|
13
|
+
* @returns a promise resolving after registration.
|
|
14
|
+
*/
|
|
15
|
+
declare const apply: (ctx: any) => Promise<void>;
|
|
16
|
+
export { apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-shell-tool.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-shell-tool/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = '@elinpf/dsh-ops-shell-tool';
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = 'ops-shell-tool-invariant';
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ['invariants'];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package is a pure library — it appends no
|
|
13
|
+
* session events and owns no durable shape. Its only state is per-call
|
|
14
|
+
* credential tokens, created and dropped inside a single execute.
|
|
15
|
+
*/
|
|
16
|
+
const install = () => { };
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Context carrying the invariant service.
|
|
20
|
+
* @returns a promise resolving after registration.
|
|
21
|
+
*/
|
|
22
|
+
const apply = async (ctx) => {
|
|
23
|
+
ctx.invariants.register(PACKAGE_NAME, install);
|
|
24
|
+
};
|
|
25
|
+
export { apply, inject, name };
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the ops shell tool factory.
|
|
3
|
+
*
|
|
4
|
+
* Types only — every runtime value (the credential token machinery, the
|
|
5
|
+
* output contract, the factory itself) lives in index.ts.
|
|
6
|
+
*
|
|
7
|
+
* @module @elinpf/dsh-ops-shell-tool
|
|
8
|
+
*/
|
|
9
|
+
import type { AccessAgent } from '@elinpf/dsh-ops-access';
|
|
10
|
+
import type { ShellExecRequest } from '@deepseek-ai/dsh-shell';
|
|
11
|
+
/** Shell tool result — the standard shape for ops command tools. */
|
|
12
|
+
export interface ShellToolResult {
|
|
13
|
+
exitCode: number;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
command: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Everything a consumer tool must supply; the factory owns the rest. */
|
|
20
|
+
export interface ProfiledShellToolSpec {
|
|
21
|
+
/** Tool name as the model sees it (e.g. 'kubectl', 'ceph', 'ssh'). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** ops-access kind to resolve (e.g. 'k8s', 'ceph', 'ssh'). */
|
|
24
|
+
kind: string;
|
|
25
|
+
/** Name of the profile-name parameter (e.g. 'cluster', 'host'). */
|
|
26
|
+
targetParam: string;
|
|
27
|
+
/** Tool description. */
|
|
28
|
+
description: string;
|
|
29
|
+
/** Description for the profile-name parameter. */
|
|
30
|
+
targetParamDescription: string;
|
|
31
|
+
/** Description for the command parameter. */
|
|
32
|
+
commandDescription: string;
|
|
33
|
+
/**
|
|
34
|
+
* Assemble the full shell command from resolved profile fields + the
|
|
35
|
+
* model's command arg. Mark every file-bearing field with `ref(field)` —
|
|
36
|
+
* it returns a display token `<id@tier:field>` that the factory swaps for
|
|
37
|
+
* the shell-quoted real value at execution time and scrubs back out of all
|
|
38
|
+
* captured output. Inline only non-secret values (flags, user@host, names).
|
|
39
|
+
*/
|
|
40
|
+
buildCommand: (fields: Record<string, unknown>, command: string, ref: CredentialRef) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Per-call shell timeout in ms (default 30000). Deployment-varying: slow
|
|
43
|
+
* batch operations and distant clusters legitimately need more, and the
|
|
44
|
+
* kill note tells the model exactly which ceiling it hit.
|
|
45
|
+
*/
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Known-noise stderr line patterns: any captured stderr line matching one
|
|
49
|
+
* of these regexes is dropped from the result. For warnings the CLI prints
|
|
50
|
+
* on every call that carry no information (e.g. ceph's missing-default-
|
|
51
|
+
* keyring lines when credentials arrive via --keyring). Keep them exact —
|
|
52
|
+
* every other stderr line passes through verbatim. Applied after
|
|
53
|
+
* credential scrubbing. Use plain (non-global) regexes.
|
|
54
|
+
*/
|
|
55
|
+
stderrNoise?: RegExp[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mints a credential display token for one resolved profile field and
|
|
59
|
+
* registers the field's value for execution-time substitution and output
|
|
60
|
+
* scrubbing. Throws when the field is absent or not a non-empty string —
|
|
61
|
+
* ref() exists for credential file fields, not optional inline values.
|
|
62
|
+
*/
|
|
63
|
+
export type CredentialRef = (field: string) => string;
|
|
64
|
+
/**
|
|
65
|
+
* The exec context the factory's execute runs under. Structurally a subset of
|
|
66
|
+
* dsh's ToolRunContext: `signal` (required there, optional here for tests)
|
|
67
|
+
* and the optional caller `agent`, whose `id` is the session the access gate
|
|
68
|
+
* keys grants on. The factory passes `agent` straight through to resolve —
|
|
69
|
+
* consumers stay identity-only and need no changes.
|
|
70
|
+
*/
|
|
71
|
+
export interface ShellToolExec {
|
|
72
|
+
signal?: ShellExecRequest['signal'];
|
|
73
|
+
agent?: AccessAgent;
|
|
74
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elinpf/dsh-ops-shell-tool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared factory for ops-access consumer tools: standard shell result shape, output schema, render, and the resolve-per-call execute template.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./invariant": {
|
|
14
|
+
"types": "./lib/invariant.d.ts",
|
|
15
|
+
"default": "./lib/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./types": {
|
|
18
|
+
"types": "./lib/types.d.ts",
|
|
19
|
+
"default": "./lib/types.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/index.js",
|
|
25
|
+
"lib/invariant.js",
|
|
26
|
+
"lib/types.js",
|
|
27
|
+
"lib/**/*.d.ts"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
31
|
+
"@elinpf/dsh-ops-access": "^0.1.0",
|
|
32
|
+
"@deepseek-ai/dsh-shell": "^0.0.1-rc.1",
|
|
33
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
37
|
+
"@deepseek-ai/dsh-shell": "0.0.1-rc.5",
|
|
38
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
|
|
39
|
+
"typescript": "^5.4.0",
|
|
40
|
+
"vitest": "^4.1.11",
|
|
41
|
+
"@elinpf/dsh-ops-access": "0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
}
|
|
52
|
+
}
|