@try-works/dsh-righthand 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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * guard-tools — DSH-native policy primitive over the tools pre-execute seam.
3
+ * Demonstrates the §4.4 allow/deny/ask gate the righthand plugin uses to
4
+ * gate high-impact deploy/invoke tools. Built on ctx.on('tools/pre-execute'),
5
+ * not a hand-rolled wrapper.
6
+ * @module dsh-righthand/guard-tools
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+
11
+ export const name = 'righthand-guard'
12
+ export const inject = ['tools']
13
+
14
+ export interface GuardRule {
15
+ /** Tool name prefix to gate (e.g. 'rh_' gates every righthand tool). */
16
+ toolPrefix: string
17
+ /** 'allow' passes through; 'deny' blocks; 'ask' defers to a policy function. */
18
+ mode: 'allow' | 'deny' | 'ask'
19
+ /** Only consulted when mode is 'ask'. Return true to allow. */
20
+ ask?: (args: unknown) => boolean
21
+ }
22
+
23
+ /** Apply a guard over a set of tool-name prefixes. */
24
+ export function apply(ctx: Context, config: { rules?: GuardRule[] } = {}): void {
25
+ const rules = config.rules ?? []
26
+
27
+ ctx.on('tools/pre-execute', (exec, next) => {
28
+ const rule = rules.find(r => exec.name.startsWith(r.toolPrefix))
29
+ if (rule === undefined) return next()
30
+ if (rule.mode === 'deny') {
31
+ throw new Error(`tool ${exec.name} is denied by righthand guard`)
32
+ }
33
+ if (rule.mode === 'ask') {
34
+ const allowed = rule.ask?.(exec.arguments) ?? false
35
+ if (!allowed) throw new Error(`tool ${exec.name} requires approval (ask returned false)`)
36
+ return next()
37
+ }
38
+ return next()
39
+ }, { prepend: true })
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * dsh-righthand — DeepSeek Harness plugin.
3
+ * DSH-native righthand tools: a durable KV store, credential/settings
4
+ * management, governed command execution, and a tool guard — all built on
5
+ * the harness's own services (storageDomain, credentials, settings,
6
+ * subprocess, jobs, tools), not hand-rolled primitives.
7
+ *
8
+ * The package's plugin entry (name/inject/apply) mounts the four tool
9
+ * modules as child fibers; each child waits on its own services, so a
10
+ * profile without `storageDomain` (e.g. headless) simply gets the other
11
+ * nine tools instead of failing the boot.
12
+ *
13
+ * Modules (each individually mountable via `export * as ...`):
14
+ * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
15
+ * - secrets-tools: rh_credential_describe/set/unset + rh_settings_get/set
16
+ * - exec-tools: rh_run / rh_run_bg over ctx.subprocess + ctx.jobs
17
+ * - guard-tools: tools/pre-execute policy (config.rules)
18
+ *
19
+ * @module @try-works/dsh-righthand
20
+ */
21
+
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+ import * as storeTools from './store-tools.ts'
24
+ import * as secretsTools from './secrets-tools.ts'
25
+ import * as execTools from './exec-tools.ts'
26
+ import * as guardTools from './guard-tools.ts'
27
+ import type { GuardRule } from './guard-tools.ts'
28
+
29
+ export const name = 'dsh-righthand'
30
+
31
+ /** The combined plugin needs no services itself; each child waits on its own. */
32
+ export const inject: string[] = []
33
+
34
+ /** Combined-plugin configuration. */
35
+ export interface RighthandConfig {
36
+ /** guard-tools policy rules; omitted rules leave the guard inert. */
37
+ rules?: GuardRule[]
38
+ }
39
+
40
+ /**
41
+ * Mount the four righthand tool modules as child fibers.
42
+ * Each child declares its own service inject, so a profile missing one
43
+ * service (e.g. `storageDomain` outside the web profile) skips that family
44
+ * instead of failing the whole plugin.
45
+ */
46
+ export function apply(ctx: Context, config: RighthandConfig = {}): void {
47
+ ctx.plugin(storeTools)
48
+ ctx.plugin(secretsTools)
49
+ ctx.plugin(execTools)
50
+ ctx.plugin(guardTools, { rules: config.rules ?? [] })
51
+ }
52
+
53
+ // Individual modules stay importable for selective mounting.
54
+ export { storeTools, secretsTools, execTools, guardTools }
55
+ export type { GuardRule } from './guard-tools.ts'
56
+
@@ -0,0 +1,147 @@
1
+ /**
2
+ * secrets-tools — DSH-native tools over ctx.credentials + ctx.settings.
3
+ * The righthand plugin's auth surface: declare a credential reference,
4
+ * describe it (never the value), set/unset through the provider, and read
5
+ * plugin settings from a registered namespace (redacted).
6
+ * Built on the harness's own seams, not a hand-rolled secret store.
7
+ * @module dsh-righthand/secrets-tools
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import { defineTool } from '@deepseek-ai/dsh-tools'
12
+ import { credentialRef } from '@deepseek-ai/dsh-credentials'
13
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
14
+ import z from '@deepseek-ai/schemastery'
15
+
16
+ export const name = 'righthand-secrets'
17
+ export const inject = ['tools', 'credentials', 'settings']
18
+
19
+ /** righthand plugin settings namespace schema. */
20
+ export interface RighthandSettings {
21
+ accountId: string
22
+ defaultScriptPrefix: string
23
+ defaultZone: string
24
+ }
25
+
26
+ export const righthandSettingsSchema = z.object({
27
+ accountId: z.string().default(''),
28
+ defaultScriptPrefix: z.string().default('rh-'),
29
+ defaultZone: z.string().default(''),
30
+ })
31
+
32
+ /** Register the settings namespace + the secret/credential tools. */
33
+ export function apply(ctx: Context): void {
34
+ const ns = settingsNamespace('righthand')
35
+ // Register once; the settings provider merges schema defaults + user doc.
36
+ const scope = ctx.settings.register(ns, righthandSettingsSchema)
37
+
38
+ ctx.tools.register(defineTool({
39
+ name: 'rh_credential_describe',
40
+ description: 'Report whether a credential reference is configured (and from which source layer), without ever returning the secret value. Use this to check auth state before a deploy/invoke.',
41
+ parameters: {
42
+ ref: { type: 'string', required: true, description: 'Credential reference, e.g. CLOUDFLARE_API_TOKEN.' },
43
+ },
44
+ output: {
45
+ schema: {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ properties: {
49
+ ref: { type: 'string', required: true },
50
+ configured: { type: 'boolean', required: true },
51
+ source: { type: 'string' },
52
+ writable: { type: 'boolean', required: true },
53
+ },
54
+ },
55
+ render: (_args, value) => [{ type: 'text', text: `${value.ref}: configured=${value.configured}${value.source ? ' (source=' + value.source + ')' : ''}, writable=${value.writable}` }],
56
+ },
57
+ async execute(args) {
58
+ const ref = credentialRef(args.ref)
59
+ const info = await ctx.credentials.describe(ref)
60
+ return { ref: args.ref, configured: info.configured, ...info.source !== undefined ? { source: info.source } : {}, writable: info.writable }
61
+ },
62
+ }))
63
+
64
+ ctx.tools.register(defineTool({
65
+ name: 'rh_credential_set',
66
+ description: 'Store a secret value for a credential reference through the harness credential provider. The value is written durably and never echoed back.',
67
+ parameters: {
68
+ ref: { type: 'string', required: true, description: 'Credential reference to store, e.g. CLOUDFLARE_API_TOKEN.' },
69
+ value: { type: 'string', required: true, description: 'The non-empty secret value to store.' },
70
+ },
71
+ output: {
72
+ schema: {
73
+ type: 'object',
74
+ additionalProperties: false,
75
+ properties: {
76
+ ref: { type: 'string', required: true },
77
+ stored: { type: 'boolean', required: true },
78
+ },
79
+ },
80
+ render: (_args, value) => [{ type: 'text', text: `stored credential ${value.ref} (value not echoed)` }],
81
+ },
82
+ async execute(args) {
83
+ if (args.value.length === 0) throw new Error('credential value must be non-empty')
84
+ await ctx.credentials.set(credentialRef(args.ref), args.value)
85
+ return { ref: args.ref, stored: true }
86
+ },
87
+ }))
88
+
89
+ ctx.tools.register(defineTool({
90
+ name: 'rh_credential_unset',
91
+ description: 'Remove a credential reference. Idempotent.',
92
+ parameters: {
93
+ ref: { type: 'string', required: true, description: 'Credential reference to remove.' },
94
+ },
95
+ output: {
96
+ schema: {
97
+ type: 'object',
98
+ additionalProperties: false,
99
+ properties: {
100
+ ref: { type: 'string', required: true },
101
+ removed: { type: 'boolean', required: true },
102
+ },
103
+ },
104
+ render: (_args, value) => [{ type: 'text', text: `removed credential ${value.ref}` }],
105
+ },
106
+ async execute(args) {
107
+ await ctx.credentials.unset(credentialRef(args.ref))
108
+ return { ref: args.ref, removed: true }
109
+ },
110
+ }))
111
+
112
+ ctx.tools.register(defineTool({
113
+ name: 'rh_settings_get',
114
+ description: 'Read the resolved righthand plugin settings (schema defaults + user overrides). Secret fields are redacted; use this to see effective account/zone/prefix configuration.',
115
+ parameters: {},
116
+ output: {
117
+ schema: { type: 'object', additionalProperties: true },
118
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
119
+ },
120
+ async execute() {
121
+ const v = scope.get()
122
+ return { accountId: v.accountId, defaultScriptPrefix: v.defaultScriptPrefix, defaultZone: v.defaultZone }
123
+ },
124
+ }))
125
+
126
+ ctx.tools.register(defineTool({
127
+ name: 'rh_settings_set',
128
+ description: 'Merge a partial patch into the righthand settings namespace (persisted by the harness settings provider).',
129
+ parameters: {
130
+ patch: { type: 'object', additionalProperties: true, required: true, description: 'Partial settings patch, e.g. { "accountId": "..." }.' },
131
+ },
132
+ output: {
133
+ schema: {
134
+ type: 'object',
135
+ additionalProperties: false,
136
+ properties: {
137
+ applied: { type: 'boolean', required: true },
138
+ },
139
+ },
140
+ render: () => [{ type: 'text', text: 'settings updated' }],
141
+ },
142
+ async execute(args) {
143
+ await scope.update(args.patch as Partial<RighthandSettings>)
144
+ return { applied: true }
145
+ },
146
+ }))
147
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * store-tools — DSH-native tools over ctx.storageDomain (domain KV).
3
+ * A generic, schema-validated key-value store surface the righthand plugin
4
+ * can reuse for its tool catalog, plus any other plugin needing durable state.
5
+ * Built on the harness's own storage domain facility, not a hand-rolled file.
6
+ * @module dsh-righthand/store-tools
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import { defineTool } from '@deepseek-ai/dsh-tools'
11
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
12
+ import type { Domain } from '@deepseek-ai/dsh-storage-domain'
13
+ import { z } from 'zod'
14
+
15
+ export const name = 'righthand-store'
16
+ export const inject = ['tools', 'storageDomain']
17
+
18
+ /** A value that JSON.stringify can carry (objects, arrays, scalars — no functions/symbols). */
19
+ export type JsonLike = unknown
20
+
21
+ /** One stored record: a string key → JSON value, with a write timestamp. */
22
+ export interface StoreRow {
23
+ value: JsonLike
24
+ updatedAt: string
25
+ }
26
+
27
+ /** Domain spec: the catalog table + a global singleton (write counter). */
28
+ export const storeDomain = defineDomain({
29
+ name: 'righthand_store',
30
+ version: 1,
31
+ tables: {
32
+ rows: domainTable(z.object({
33
+ value: z.unknown(),
34
+ updatedAt: z.string(),
35
+ })),
36
+ },
37
+ global: {
38
+ schema: z.object({ writes: z.number() }),
39
+ initial: { writes: 0 },
40
+ },
41
+ })
42
+
43
+ export type StoreDomain = Domain<typeof storeDomain>
44
+
45
+ /** Persisted store with typed read/write over the domain tables. */
46
+ export class KeyValueStore {
47
+ constructor(private readonly domain: StoreDomain) {}
48
+
49
+ async get(key: string): Promise<StoreRow | undefined> {
50
+ return this.domain.table('rows').get(key) as StoreRow | undefined
51
+ }
52
+
53
+ async put(key: string, value: JsonLike): Promise<StoreRow> {
54
+ const row: StoreRow = { value, updatedAt: new Date().toISOString() }
55
+ await this.domain.table('rows').put(key, row)
56
+ const writes = this.domain.global.get()
57
+ await this.domain.global.set({ writes: writes.writes + 1 })
58
+ return row
59
+ }
60
+
61
+ async delete(key: string): Promise<boolean> {
62
+ return this.domain.table('rows').delete(key)
63
+ }
64
+
65
+ async list(): Promise<string[]> {
66
+ return [...this.domain.table('rows').keys()]
67
+ }
68
+
69
+ async writes(): Promise<number> {
70
+ return this.domain.global.get().writes
71
+ }
72
+ }
73
+
74
+ /** Open the store on the mounted domain facility and register the model-facing tools. */
75
+ export function apply(ctx: Context): void {
76
+ let store: KeyValueStore | undefined
77
+ let ready: Promise<void> | undefined
78
+
79
+ // Open the domain asynchronously (the facility may still be activating).
80
+ ctx.effect(() => {
81
+ ready = (async () => {
82
+ const domain = await ctx.storageDomain.open(storeDomain)
83
+ store = new KeyValueStore(domain)
84
+ })()
85
+ return () => { /* domain facility owns the domain; closing happens at unmount */ }
86
+ })
87
+
88
+ const ensure = async (): Promise<KeyValueStore> => {
89
+ if (store === undefined) { await ready; }
90
+ if (store === undefined) throw new Error('righthand store is not open (storageDomain not mounted)')
91
+ return store
92
+ }
93
+
94
+ ctx.tools.register(defineTool({
95
+ name: 'rh_store_put',
96
+ description: 'Durably store a JSON value under a string key in the righthand KV store. Returns the stored record with a timestamp.',
97
+ parameters: {
98
+ key: { type: 'string', required: true, description: 'String key for the record.' },
99
+ value: { type: 'json', required: true, description: 'Any JSON value to store (object, array, string, number, boolean, null).' },
100
+ },
101
+ output: {
102
+ schema: {
103
+ type: 'object',
104
+ additionalProperties: false,
105
+ properties: {
106
+ key: { type: 'string', required: true },
107
+ updatedAt: { type: 'string', required: true },
108
+ writes: { type: 'integer', required: true },
109
+ },
110
+ },
111
+ render: (_args, value) => [{ type: 'text', text: `stored ${value.key} (writes=${value.writes})` }],
112
+ },
113
+ async execute(args) {
114
+ const s = await ensure()
115
+ const row = await s.put(args.key, args.value)
116
+ return { key: args.key, updatedAt: row.updatedAt, writes: await s.writes() }
117
+ },
118
+ }))
119
+
120
+ ctx.tools.register(defineTool({
121
+ name: 'rh_store_get',
122
+ description: 'Read a stored JSON value by key. Returns null when the key is absent.',
123
+ parameters: {
124
+ key: { type: 'string', required: true, description: 'String key to read.' },
125
+ },
126
+ output: {
127
+ schema: {
128
+ type: 'object',
129
+ additionalProperties: false,
130
+ properties: {
131
+ found: { type: 'boolean', required: true },
132
+ key: { type: 'string', required: true },
133
+ value: { type: 'json' },
134
+ updatedAt: { type: 'string' },
135
+ },
136
+ },
137
+ render: (_args, value) => [{ type: 'text', text: value.found ? `${value.key} = ${JSON.stringify(value.value)}` : `${value.key} (absent)` }],
138
+ },
139
+ async execute(args) {
140
+ const s = await ensure()
141
+ const row = await s.get(args.key)
142
+ if (row === undefined) return { found: false, key: args.key }
143
+ return { found: true, key: args.key, value: row.value as any, updatedAt: row.updatedAt }
144
+ },
145
+ }))
146
+
147
+ ctx.tools.register(defineTool({
148
+ name: 'rh_store_delete',
149
+ description: 'Delete a stored record by key. Returns whether it existed.',
150
+ parameters: {
151
+ key: { type: 'string', required: true, description: 'String key to delete.' },
152
+ },
153
+ output: {
154
+ schema: {
155
+ type: 'object',
156
+ additionalProperties: false,
157
+ properties: {
158
+ key: { type: 'string', required: true },
159
+ existed: { type: 'boolean', required: true },
160
+ },
161
+ },
162
+ render: (_args, value) => [{ type: 'text', text: `deleted ${value.key}: ${value.existed}` }],
163
+ },
164
+ async execute(args) {
165
+ const s = await ensure()
166
+ const existed = await s.delete(args.key)
167
+ return { key: args.key, existed }
168
+ },
169
+ }))
170
+
171
+ ctx.tools.register(defineTool({
172
+ name: 'rh_store_list',
173
+ description: 'List all keys currently in the righthand KV store.',
174
+ parameters: {},
175
+ output: {
176
+ schema: { type: 'array', items: { type: 'string' } },
177
+ render: (_args, keys) => [{ type: 'text', text: keys.length === 0 ? '(empty)' : keys.join('\n') }],
178
+ },
179
+ async execute() {
180
+ const s = await ensure()
181
+ return s.list()
182
+ },
183
+ }))
184
+ }