@try-works/dsh-righthand 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@ tool guard. Every tool is built on the harness's own services
6
6
  (`storageDomain`, `credentials`, `settings`, `subprocess`, `jobs`, `tools`),
7
7
  not hand-rolled primitives.
8
8
 
9
+ The plugin also ships **blueprint guidance** — ten named recipes for the agent-built tools this plugin exists to compose (see [Blueprints](#blueprints) below) — plus a packaged skill, a settings namespace, and an optional tool guard.
10
+
9
11
  ## Install
10
12
 
11
13
  ```bash
@@ -23,6 +25,7 @@ dsh --profile <profile> --patch ./cordis.patch.yml
23
25
  | Tool | Family | Backing service |
24
26
  |---|---|---|
25
27
  | `rh_store_put` / `rh_store_get` / `rh_store_delete` / `rh_store_list` | store | `ctx.storageDomain` (domain KV) |
28
+ | `rh_task_create` / `rh_task_list` / `rh_task_next` / `rh_task_update` / `rh_task_delete` | tasks | `ctx.storageDomain` (typed `righthand_tasks` domain; state machine open → done/failed) |
26
29
  | `rh_credential_describe` / `rh_credential_set` / `rh_credential_unset` | secrets | `ctx.credentials` (values never echoed) |
27
30
  | `rh_settings_get` / `rh_settings_set` | secrets | `ctx.settings` (namespace `righthand`) |
28
31
  | `rh_run` | exec | `ctx.subprocess` (collect mode, bounded output) |
@@ -46,6 +49,13 @@ dsh --profile <profile> --patch ./cordis.patch.yml
46
49
  Guard modes: `allow` (pass through), `deny` (block with an error), `ask`
47
50
  (defer to the policy function). Omitted rules leave the guard inert.
48
51
 
52
+ Rules may also declare `destructive: true` — a documentation flag for
53
+ irreversible effects (deploy, delete, DNS change). It does not change
54
+ enforcement; it records *why* the prefix is gated, and
55
+ `tests/permissions.golden` records every `rh_*` tool's derived guard facts so
56
+ guard changes are never silent (run `UPDATE_GOLDEN=1 pnpm test` to
57
+ re-baseline deliberately).
58
+
49
59
  ## Settings
50
60
 
51
61
  Namespace "righthand" (registered by the secrets family):
@@ -59,12 +69,79 @@ Namespace "righthand" (registered by the secrets family):
59
69
  `rh_settings_get` returns the resolved values; `rh_settings_set` merges a
60
70
  partial patch (persisted by the harness settings provider).
61
71
 
72
+ ## Blueprints
73
+
74
+ Blueprint guidance lives under `experiments/<blueprint>/` — each a
75
+ `blueprint.json` spec plus a runnable template kit (`run.ts` + `test.ts`,
76
+ `LEARNINGS.md`, and for the cloud-tested ones a `cloud/` Worker). They
77
+ describe the tools this plugin composes; the daily-digest kit is the
78
+ canonical example of the current DSH-native form (tool-to-tool via
79
+ `ctx.tools.execute`, synthesis via `ctx.llm`, deliverables via `ctx.fs`).
80
+
81
+ | Blueprint | What it builds |
82
+ |---|---|
83
+ | `blueprint/daily-digest` | Collect sources, summarize, send on a schedule — extractive locally, LLM synthesis as escalation |
84
+ | `blueprint/research-radar` | Multi-source research over a rolling window (Reddit, X, YouTube, HN, Polymarket, web): score, dedupe, synthesize with citations |
85
+ | `blueprint/web-scraper` | Fetch a page, extract title/headings/links/text, make it searchable — static HTML first, render/vision as escalation |
86
+ | `blueprint/vision-worker` | Cloudflare Worker exposing kimi-k2.6 vision: route image requests for text-only models, return a versioned `VisionEnvelope` |
87
+ | `blueprint/vision-qa-assistant` | Answer questions about images for a text-only model — hash-cache locally, Workers AI vision as the semantic path |
88
+ | `blueprint/inbound-webhook-pipeline` | Receive → validate → run job → store → notify, with idempotency-key dedupe as the load-bearing invariant |
89
+ | `blueprint/event-sourced-store` | Tool store where every mutation is an event: govern, checksum, append offset-addressable events, catch up by version |
90
+ | `blueprint/governed-registry` | Register a tool format; every write runs the governed flow: validate → normalize → checksum → index → summarize |
91
+ | `blueprint/mcp-tool-surface` | Deterministic + AI-backed tools behind a stateless MCP surface with normalized LLM output |
92
+ | `blueprint/session-watcher` | Poll DSH sessions, evaluate a condition (new publish / builder idle / cadence), message a target session |
93
+
94
+ Analyses of the [micro.mu](https://github.com/micro/mu) project this plugin
95
+ learns from: [`docs/agent-tool-catalog.md`](docs/agent-tool-catalog.md) (tool
96
+ families worth adding, from the tools catalogue + permission commit) and
97
+ [`docs/mu-repo-analysis.md`](docs/mu-repo-analysis.md) (architecture lessons
98
+ from the whole repo — single-source specs, golden permissions, layering,
99
+ held-state gates, SSRF guarding).
100
+
101
+ ## Packaged skill
102
+
103
+ The plugin ships the `dsh-righthand` skill
104
+ (`skills/dsh-righthand/SKILL.md`, the dsh-plugin packaged-skill standard):
105
+ tool-reference tables, store/credential/settings semantics, guard modes, and
106
+ service availability. It registers via `ctx.skills` on mount, so the agent's
107
+ skill catalog lists it automatically.
108
+
109
+ ## Use cases
110
+
111
+ Everything below runs through the harness's own services; nothing is
112
+ hand-rolled.
113
+
114
+ **Durable agent memory / task board.** Store task cards under `task:<slug>`
115
+ and let `rh_store_list` enumerate them across restarts and context
116
+ compaction — no project files touched. Complete by overwriting, clean up
117
+ with `rh_store_delete`.
118
+
119
+ **Pre-deploy gate.** Before any deploy: `rh_credential_describe` asserts the
120
+ API token exists, `rh_settings_get` asserts the account/zone, `rh_run` runs
121
+ tests + build, `rh_store_put` records a deploy receipt, and a guard rule
122
+ gates the actual deploy tool behind `mode: 'ask'`.
123
+
124
+ **Secret onboarding.** `rh_credential_set` stores a key through the
125
+ credential provider (the value is never echoed back); `rh_credential_describe`
126
+ reports configured state + source; rotation is unset-then-set.
127
+
128
+ **Daily digests.** The agent fetches sources and stores them under
129
+ `digest:<date>` (see `blueprint/daily-digest`), diffing days with
130
+ `rh_store_get` and rendering MDX deliverables through `ctx.fs`.
131
+
132
+ **CI-lite.** `rh_run_bg` starts a long build as an owner-scoped background
133
+ job; its id + status live in the store so a later turn can poll and collect.
134
+
135
+ **Governed exec.** Any high-impact command can be fronted by a guard rule
136
+ (`deny` to block a prefix, `ask` to require a policy function's approval),
137
+ so the agent cannot deploy or destroy without the gate.
138
+
62
139
  ## Service availability
63
140
 
64
- The store family needs `ctx.storageDomain`, which the web profile provides
65
- (`storage` + `storage-json` + `storage-domain` rows). In a profile without
66
- it — e.g. the headless bundle — the store tools stay dormant and the other
67
- seven tools still register; the plugin never fails the boot.
141
+ The store and task families need `ctx.storageDomain`, which the web profile
142
+ provides (`storage` + `storage-json` + `storage-domain` rows). In a profile
143
+ without it — e.g. the headless bundle — those nine tools stay dormant and the
144
+ other seven tools still register; the plugin never fails the boot.
68
145
 
69
146
  ## Development
70
147
 
@@ -3,6 +3,14 @@
3
3
  * Demonstrates the §4.4 allow/deny/ask gate the righthand plugin uses to
4
4
  * gate high-impact deploy/invoke tools. Built on ctx.on('tools/pre-execute'),
5
5
  * not a hand-rolled wrapper.
6
+ *
7
+ * Rules are prefix-based. A rule may also declare `destructive: true` — a
8
+ * pure documentation flag saying the gated tools have irreversible effects
9
+ * (deploy, delete, DNS change): the kind of surface a tool *description*
10
+ * (attacker-controlled text) could talk the agent into. The flag does not
11
+ * change enforcement — `mode` does — it records WHY the prefix is guarded,
12
+ * and `tests/permissions.golden` records it so guard changes are never
13
+ * silent.
6
14
  * @module dsh-righthand/guard-tools
7
15
  */
8
16
  import type { Context } from '@deepseek-ai/cordis';
@@ -13,9 +21,30 @@ export interface GuardRule {
13
21
  toolPrefix: string;
14
22
  /** 'allow' passes through; 'deny' blocks; 'ask' defers to a policy function. */
15
23
  mode: 'allow' | 'deny' | 'ask';
24
+ /**
25
+ * Documentation flag: the gated tools have irreversible effects nobody
26
+ * asked for (a deploy, a delete, a DNS change). Declarative only —
27
+ * enforcement is the rule's `mode`. Default false.
28
+ */
29
+ destructive?: boolean;
16
30
  /** Only consulted when mode is 'ask'. Return true to allow. */
17
31
  ask?: (args: unknown) => boolean;
18
32
  }
33
+ /**
34
+ * The guard facts a tool name resolves to: the mode the first matching rule
35
+ * enforces (or 'none' when no rule matches) and whether that rule marks the
36
+ * prefix destructive. This is the SINGLE lookup both the enforcement hook
37
+ * and the permissions golden use, so the recorded facts cannot drift from
38
+ * what the gate actually does.
39
+ */
40
+ export interface GuardFacts {
41
+ mode: 'allow' | 'deny' | 'ask' | 'none';
42
+ destructive: boolean;
43
+ /** The matched rule, when one matched — the enforcement hook's ask path needs it. */
44
+ rule: GuardRule | undefined;
45
+ }
46
+ /** Resolve one tool name to its guard facts. Rules match in order, first wins. */
47
+ export declare function guardFactsFor(toolName: string, rules: readonly GuardRule[]): GuardFacts;
19
48
  /** Apply a guard over a set of tool-name prefixes. */
20
49
  export declare function apply(ctx: Context, config?: {
21
50
  rules?: GuardRule[];
@@ -1 +1 @@
1
- {"version":3,"file":"guard-tools.d.ts","sourceRoot":"","sources":["../src/guard-tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,oBAAoB,CAAA;AACrC,eAAO,MAAM,MAAM,UAAY,CAAA;AAE/B,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAA;IAClB,gFAAgF;IAChF,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;CACjC;AAED,sDAAsD;AACtD,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CAAO,GAAG,IAAI,CAgB9E"}
1
+ {"version":3,"file":"guard-tools.d.ts","sourceRoot":"","sources":["../src/guard-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,oBAAoB,CAAA;AACrC,eAAO,MAAM,MAAM,UAAY,CAAA;AAE/B,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAA;IAClB,gFAAgF;IAChF,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;IACvC,WAAW,EAAE,OAAO,CAAA;IACpB,qFAAqF;IACrF,IAAI,EAAE,SAAS,GAAG,SAAS,CAAA;CAC5B;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,UAAU,CAIvF;AAED,sDAAsD;AACtD,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CAAO,GAAG,IAAI,CAa9E"}
@@ -3,26 +3,38 @@
3
3
  * Demonstrates the §4.4 allow/deny/ask gate the righthand plugin uses to
4
4
  * gate high-impact deploy/invoke tools. Built on ctx.on('tools/pre-execute'),
5
5
  * not a hand-rolled wrapper.
6
+ *
7
+ * Rules are prefix-based. A rule may also declare `destructive: true` — a
8
+ * pure documentation flag saying the gated tools have irreversible effects
9
+ * (deploy, delete, DNS change): the kind of surface a tool *description*
10
+ * (attacker-controlled text) could talk the agent into. The flag does not
11
+ * change enforcement — `mode` does — it records WHY the prefix is guarded,
12
+ * and `tests/permissions.golden` records it so guard changes are never
13
+ * silent.
6
14
  * @module dsh-righthand/guard-tools
7
15
  */
8
16
  export const name = 'righthand-guard';
9
17
  export const inject = ['tools'];
18
+ /** Resolve one tool name to its guard facts. Rules match in order, first wins. */
19
+ export function guardFactsFor(toolName, rules) {
20
+ const rule = rules.find(r => toolName.startsWith(r.toolPrefix));
21
+ if (rule === undefined)
22
+ return { mode: 'none', destructive: false, rule: undefined };
23
+ return { mode: rule.mode, destructive: rule.destructive === true, rule };
24
+ }
10
25
  /** Apply a guard over a set of tool-name prefixes. */
11
26
  export function apply(ctx, config = {}) {
12
27
  const rules = config.rules ?? [];
13
28
  ctx.on('tools/pre-execute', (exec, next) => {
14
- const rule = rules.find(r => exec.name.startsWith(r.toolPrefix));
15
- if (rule === undefined)
29
+ const facts = guardFactsFor(exec.name, rules);
30
+ if (facts.mode === 'none' || facts.mode === 'allow')
16
31
  return next();
17
- if (rule.mode === 'deny') {
32
+ if (facts.mode === 'deny') {
18
33
  throw new Error(`tool ${exec.name} is denied by righthand guard`);
19
34
  }
20
- if (rule.mode === 'ask') {
21
- const allowed = rule.ask?.(exec.arguments) ?? false;
22
- if (!allowed)
23
- throw new Error(`tool ${exec.name} requires approval (ask returned false)`);
24
- return next();
25
- }
35
+ const allowed = facts.rule?.ask?.(exec.arguments) ?? false;
36
+ if (!allowed)
37
+ throw new Error(`tool ${exec.name} requires approval (ask returned false)`);
26
38
  return next();
27
39
  }, { prepend: true });
28
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"guard-tools.js","sourceRoot":"","sources":["../src/guard-tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAA;AACrC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,CAAA;AAW/B,sDAAsD;AACtD,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAAkC,EAAE;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAA;IAEhC,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;QACzC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;QAChE,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,EAAE,CAAA;QACrC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAA;QACnE,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACxB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,CAAA;YACnD,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,yCAAyC,CAAC,CAAA;YACzF,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QACD,OAAO,IAAI,EAAE,CAAA;IACf,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;AACvB,CAAC"}
1
+ {"version":3,"file":"guard-tools.js","sourceRoot":"","sources":["../src/guard-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAA;AACrC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,CAAA;AA+B/B,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,KAA2B;IACzE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;IAC/D,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACpF,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,IAAI,EAAE,CAAA;AAC1E,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAAkC,EAAE;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAA;IAEhC,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC7C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,IAAI,EAAE,CAAA;QAClE,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAA;QACnE,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,CAAA;QAC1D,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,yCAAyC,CAAC,CAAA;QACzF,OAAO,IAAI,EAAE,CAAA;IACf,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;AACvB,CAAC"}
package/lib/index.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * Modules (each individually mountable via `export * as ...`):
15
15
  * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
16
16
  * - secrets-tools: rh_credential_describe/set/unset + rh_settings_get/set
17
+ * - task-tools: rh_task_create/list/next/update/delete over ctx.storageDomain
17
18
  * - exec-tools: rh_run / rh_run_bg over ctx.subprocess + ctx.jobs
18
19
  * - guard-tools: tools/pre-execute policy (config.rules)
19
20
  * - skills: the packaged dsh-righthand skill (ctx.skills)
@@ -25,6 +26,7 @@ import * as storeTools from './store-tools.ts';
25
26
  import * as secretsTools from './secrets-tools.ts';
26
27
  import * as execTools from './exec-tools.ts';
27
28
  import * as guardTools from './guard-tools.ts';
29
+ import * as taskTools from './task-tools.ts';
28
30
  import * as righthandSkills from './skills.ts';
29
31
  import type { GuardRule } from './guard-tools.ts';
30
32
  export declare const name = "dsh-righthand";
@@ -42,7 +44,9 @@ export interface RighthandConfig {
42
44
  * instead of failing the whole plugin.
43
45
  */
44
46
  export declare function apply(ctx: Context, config?: RighthandConfig): void;
45
- export { storeTools, secretsTools, execTools, guardTools, righthandSkills };
47
+ export { storeTools, secretsTools, execTools, taskTools, guardTools, righthandSkills };
46
48
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from './skills.ts';
47
49
  export type { GuardRule } from './guard-tools.ts';
50
+ export { guardFactsFor } from './guard-tools.ts';
51
+ export type { GuardFacts } from './guard-tools.ts';
48
52
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,IAAI,CAMtE;AAGD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AAC3E,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACtF,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,IAAI,CAOtE;AAGD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AACtF,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACtF,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
package/lib/index.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * Modules (each individually mountable via `export * as ...`):
15
15
  * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
16
16
  * - secrets-tools: rh_credential_describe/set/unset + rh_settings_get/set
17
+ * - task-tools: rh_task_create/list/next/update/delete over ctx.storageDomain
17
18
  * - exec-tools: rh_run / rh_run_bg over ctx.subprocess + ctx.jobs
18
19
  * - guard-tools: tools/pre-execute policy (config.rules)
19
20
  * - skills: the packaged dsh-righthand skill (ctx.skills)
@@ -24,6 +25,7 @@ import * as storeTools from "./store-tools.js";
24
25
  import * as secretsTools from "./secrets-tools.js";
25
26
  import * as execTools from "./exec-tools.js";
26
27
  import * as guardTools from "./guard-tools.js";
28
+ import * as taskTools from "./task-tools.js";
27
29
  import * as righthandSkills from "./skills.js";
28
30
  export const name = 'dsh-righthand';
29
31
  /** The combined plugin needs no services itself; each child waits on its own. */
@@ -38,10 +40,12 @@ export function apply(ctx, config = {}) {
38
40
  ctx.plugin(storeTools);
39
41
  ctx.plugin(secretsTools);
40
42
  ctx.plugin(execTools);
43
+ ctx.plugin(taskTools);
41
44
  ctx.plugin(guardTools, { rules: config.rules ?? [] });
42
45
  ctx.plugin(righthandSkills);
43
46
  }
44
47
  // Individual modules stay importable for selective mounting.
45
- export { storeTools, secretsTools, execTools, guardTools, righthandSkills };
48
+ export { storeTools, secretsTools, execTools, taskTools, guardTools, righthandSkills };
46
49
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from "./skills.js";
50
+ export { guardFactsFor } from "./guard-tools.js";
47
51
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAG9C,MAAM,CAAC,MAAM,IAAI,GAAG,eAAe,CAAA;AAEnC,iFAAiF;AACjF,MAAM,CAAC,MAAM,MAAM,GAAa,EAAE,CAAA;AAQlC;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAA0B,EAAE;IAC9D,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACtB,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACxB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrB,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,CAAA;IACrD,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;AAC7B,CAAC;AAED,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AAC3E,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAG9C,MAAM,CAAC,MAAM,IAAI,GAAG,eAAe,CAAA;AAEnC,iFAAiF;AACjF,MAAM,CAAC,MAAM,MAAM,GAAa,EAAE,CAAA;AAQlC;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAA0B,EAAE;IAC9D,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACtB,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACxB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrB,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,CAAA;IACrD,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;AAC7B,CAAC;AAED,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AACtF,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * task-tools — rh_task_* over ctx.storageDomain.
3
+ * A typed task board with a state machine: open → done | failed.
4
+ * `rh_task_next` is the queue discipline — the oldest open task is what to
5
+ * work on now. Descriptions carry the rule that a task's failure is recorded
6
+ * like an answer: a run that could not happen is news the owner needs.
7
+ * @module dsh-righthand/task-tools
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import type { Domain } from '@deepseek-ai/dsh-storage-domain';
11
+ export declare const name = "righthand-task";
12
+ export declare const inject: string[];
13
+ export type TaskState = 'open' | 'done' | 'failed';
14
+ export interface Task {
15
+ id: string;
16
+ title: string;
17
+ detail: string;
18
+ due: string;
19
+ state: TaskState;
20
+ result: string;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ }
24
+ /** Domain spec: the typed task table. */
25
+ export declare const taskDomain: {
26
+ name: string;
27
+ version: number;
28
+ tables: {
29
+ tasks: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, {
30
+ id: string;
31
+ title: string;
32
+ detail: string;
33
+ due: string;
34
+ state: "failed" | "open" | "done";
35
+ result: string;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }>;
39
+ };
40
+ };
41
+ export type TaskDomain = Domain<typeof taskDomain>;
42
+ /** Open the task domain and register the model-facing tools. */
43
+ export declare function apply(ctx: Context): void;
44
+ //# sourceMappingURL=task-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../src/task-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AAG7D,eAAO,MAAM,IAAI,mBAAmB,CAAA;AACpC,eAAO,MAAM,MAAM,UAA6B,CAAA;AAEhD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AAElD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,yCAAyC;AACzC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;CAerB,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAA;AAMlD,gEAAgE;AAChE,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAgLxC"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * task-tools — rh_task_* over ctx.storageDomain.
3
+ * A typed task board with a state machine: open → done | failed.
4
+ * `rh_task_next` is the queue discipline — the oldest open task is what to
5
+ * work on now. Descriptions carry the rule that a task's failure is recorded
6
+ * like an answer: a run that could not happen is news the owner needs.
7
+ * @module dsh-righthand/task-tools
8
+ */
9
+ import { defineTool } from '@deepseek-ai/dsh-tools';
10
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
11
+ import { z } from 'zod';
12
+ export const name = 'righthand-task';
13
+ export const inject = ['tools', 'storageDomain'];
14
+ /** Domain spec: the typed task table. */
15
+ export const taskDomain = defineDomain({
16
+ name: 'righthand_tasks',
17
+ version: 1,
18
+ tables: {
19
+ tasks: domainTable(z.object({
20
+ id: z.string(),
21
+ title: z.string(),
22
+ detail: z.string(),
23
+ due: z.string(),
24
+ state: z.enum(['open', 'done', 'failed']),
25
+ result: z.string(),
26
+ createdAt: z.string(),
27
+ updatedAt: z.string(),
28
+ })),
29
+ },
30
+ });
31
+ function newId() {
32
+ return 't-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
33
+ }
34
+ /** Open the task domain and register the model-facing tools. */
35
+ export function apply(ctx) {
36
+ let domain;
37
+ let ready;
38
+ ctx.effect(() => {
39
+ ready = (async () => {
40
+ domain = await ctx.storageDomain.open(taskDomain);
41
+ })();
42
+ return () => { };
43
+ });
44
+ const ensure = async () => {
45
+ if (domain === undefined) {
46
+ await ready;
47
+ }
48
+ if (domain === undefined)
49
+ throw new Error('righthand tasks are not open (storageDomain not mounted)');
50
+ return domain;
51
+ };
52
+ const all = async () => {
53
+ const d = await ensure();
54
+ return [...d.table('tasks').entries()].map(([, t]) => t);
55
+ };
56
+ ctx.tools.register(defineTool({
57
+ name: 'rh_task_create',
58
+ description: 'Add a task with a title, optional detail and due date. Returns the created task with a generated id and state open. A task the agent cannot run is still recorded — its failure is delivered like an answer.',
59
+ parameters: {
60
+ title: { type: 'string', required: true, description: 'What the task is, e.g. "deploy vision-worker".' },
61
+ detail: { type: 'string', description: 'Optional longer description of what done means.' },
62
+ due: { type: 'string', description: 'Optional due date, YYYY-MM-DD.' },
63
+ },
64
+ output: {
65
+ schema: {
66
+ type: 'object',
67
+ additionalProperties: false,
68
+ properties: {
69
+ id: { type: 'string', required: true },
70
+ title: { type: 'string', required: true },
71
+ state: { type: 'string', required: true },
72
+ },
73
+ },
74
+ render: (_args, value) => [{ type: 'text', text: 'task ' + value.id + ' created: ' + value.title + ' (open)' }],
75
+ },
76
+ async execute(args) {
77
+ const d = await ensure();
78
+ const now = new Date().toISOString();
79
+ const task = {
80
+ id: newId(),
81
+ title: args.title.trim(),
82
+ detail: args.detail ?? '',
83
+ due: args.due ?? '',
84
+ state: 'open',
85
+ result: '',
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ };
89
+ if (task.title === '')
90
+ throw new Error('a task needs a title');
91
+ await d.table('tasks').put(task.id, task);
92
+ return { id: task.id, title: task.title, state: task.state };
93
+ },
94
+ }));
95
+ ctx.tools.register(defineTool({
96
+ name: 'rh_task_list',
97
+ description: 'List tasks, open ones first (oldest open first). Optionally filter by state.',
98
+ parameters: {
99
+ state: { type: 'string', description: 'Filter: open | done | failed. Omit for all tasks.' },
100
+ },
101
+ output: {
102
+ schema: {
103
+ type: 'array',
104
+ items: {
105
+ type: 'object',
106
+ additionalProperties: false,
107
+ properties: {
108
+ id: { type: 'string', required: true },
109
+ title: { type: 'string', required: true },
110
+ state: { type: 'string', required: true },
111
+ due: { type: 'string' },
112
+ },
113
+ },
114
+ },
115
+ render: (_args, tasks) => [{ type: 'text', text: tasks.length === 0 ? '(no tasks)' : tasks.map((t) => '[' + t.state + '] ' + t.title).join(' / ') }],
116
+ },
117
+ async execute(args) {
118
+ const tasks = await all();
119
+ const wanted = args.state ?? null;
120
+ const filtered = wanted ? tasks.filter(t => t.state === wanted) : tasks;
121
+ const rank = { open: 0, done: 1, failed: 2 };
122
+ filtered.sort((a, b) => (rank[a.state] - rank[b.state]) || a.createdAt.localeCompare(b.createdAt));
123
+ return filtered.map(t => ({ id: t.id, title: t.title, state: t.state, ...t.due ? { due: t.due } : {} }));
124
+ },
125
+ }));
126
+ ctx.tools.register(defineTool({
127
+ name: 'rh_task_next',
128
+ description: 'The next open task — what to work on now. Oldest open task first. Returns found: false when nothing is open.',
129
+ parameters: {},
130
+ output: {
131
+ schema: {
132
+ type: 'object',
133
+ additionalProperties: false,
134
+ properties: {
135
+ found: { type: 'boolean', required: true },
136
+ id: { type: 'string' },
137
+ title: { type: 'string' },
138
+ detail: { type: 'string' },
139
+ due: { type: 'string' },
140
+ },
141
+ },
142
+ render: (_args, value) => [{ type: 'text', text: value.found ? 'next: ' + value.title : '(nothing open)' }],
143
+ },
144
+ async execute() {
145
+ const tasks = await all();
146
+ const open = tasks.filter(t => t.state === 'open').sort((a, b) => a.createdAt.localeCompare(b.createdAt));
147
+ const t = open[0];
148
+ if (t === undefined)
149
+ return { found: false };
150
+ return { found: true, id: t.id, title: t.title, ...t.detail ? { detail: t.detail } : {}, ...t.due ? { due: t.due } : {} };
151
+ },
152
+ }));
153
+ ctx.tools.register(defineTool({
154
+ name: 'rh_task_update',
155
+ description: 'Change a task: its state (open | done | failed) and/or its result. A failed task should carry its result — what was tried and what went wrong — so the record says what happened, not just that something did.',
156
+ parameters: {
157
+ id: { type: 'string', required: true, description: 'The task id from rh_task_create or rh_task_list.' },
158
+ state: { type: 'string', description: 'New state: open | done | failed.' },
159
+ result: { type: 'string', description: 'What happened — the outcome, or the failure reason.' },
160
+ },
161
+ output: {
162
+ schema: {
163
+ type: 'object',
164
+ additionalProperties: false,
165
+ properties: {
166
+ found: { type: 'boolean', required: true },
167
+ id: { type: 'string', required: true },
168
+ state: { type: 'string', required: true },
169
+ },
170
+ },
171
+ render: (_args, value) => [{ type: 'text', text: value.found ? 'task ' + value.id + ' -> ' + value.state : 'task ' + value.id + ' not found' }],
172
+ },
173
+ async execute(args) {
174
+ const d = await ensure();
175
+ const task = d.table('tasks').get(args.id);
176
+ if (task === undefined)
177
+ return { found: false, id: args.id, state: 'open' };
178
+ const next = {
179
+ ...task,
180
+ ...args.state !== undefined ? { state: args.state } : {},
181
+ ...args.result !== undefined ? { result: args.result } : {},
182
+ updatedAt: new Date().toISOString(),
183
+ };
184
+ await d.table('tasks').put(args.id, next);
185
+ return { found: true, id: args.id, state: next.state };
186
+ },
187
+ }));
188
+ ctx.tools.register(defineTool({
189
+ name: 'rh_task_delete',
190
+ description: 'Remove a task by id. Returns whether it existed.',
191
+ parameters: {
192
+ id: { type: 'string', required: true, description: 'The task id to remove.' },
193
+ },
194
+ output: {
195
+ schema: {
196
+ type: 'object',
197
+ additionalProperties: false,
198
+ properties: {
199
+ id: { type: 'string', required: true },
200
+ existed: { type: 'boolean', required: true },
201
+ },
202
+ },
203
+ render: (_args, value) => [{ type: 'text', text: 'deleted ' + value.id + ': ' + value.existed }],
204
+ },
205
+ async execute(args) {
206
+ const d = await ensure();
207
+ const existed = await d.table('tasks').delete(args.id);
208
+ return { id: args.id, existed };
209
+ },
210
+ }));
211
+ }
212
+ //# sourceMappingURL=task-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-tools.js","sourceRoot":"","sources":["../src/task-tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@try-works/dsh-righthand",
3
3
  "description": "DeepSeek Harness plugin: DSH-native righthand tools — durable KV store, credential/settings management, governed exec, and a tool guard",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -12,6 +12,7 @@ The righthand toolkit: DSH-native tools over the harness's own services. Use thi
12
12
  | Family | Tools | Backing service | Use when |
13
13
  |--------|-------|-----------------|----------|
14
14
  | store | `rh_store_put` / `rh_store_get` / `rh_store_delete` / `rh_store_list` | `ctx.storageDomain` (domain KV) | The task needs durable state across turns: a catalog, counter, cache, or note. Prefer this over files for structured JSON records. |
15
+ | tasks | `rh_task_create` / `rh_task_list` / `rh_task_next` / `rh_task_update` / `rh_task_delete` | `ctx.storageDomain` (typed `righthand_tasks` domain) | A task board with a state machine (open → done/failed). `rh_task_next` is the oldest open task — what to work on now. A failed task's `result` records what went wrong: a task that cannot run is still recorded, never silently dropped. |
15
16
  | secrets | `rh_credential_describe` / `rh_credential_set` / `rh_credential_unset` | `ctx.credentials` | The task must check, store, or remove a credential reference (e.g. `CLOUDFLARE_API_TOKEN`). Values are written durably and **never echoed back**. |
16
17
  | settings | `rh_settings_get` / `rh_settings_set` | `ctx.settings` (namespace `righthand`) | Reading or patching righthand settings (`accountId`, `defaultZone`, `defaultScriptPrefix`). |
17
18
  | exec | `rh_run` / `rh_run_bg` | `ctx.subprocess` + `ctx.jobs` | Running one command (argv array, no shell interpretation). `rh_run` collects bounded output; `rh_run_bg` starts an owner-scoped background job and returns its id. |
@@ -23,6 +24,13 @@ The righthand toolkit: DSH-native tools over the harness's own services. Use thi
23
24
  - `rh_store_get` returns `{ found, key, value?, updatedAt? }` — `found: false` means the key is absent, not an error.
24
25
  - Writes are durable (backend flush before commit) and serialized on one write chain; values must be JSON-serializable.
25
26
 
27
+ ## Task semantics
28
+
29
+ - One domain `righthand_tasks`: a `tasks` table keyed by generated task id (`t-...`).
30
+ - States: `open` → `done` | `failed`. `rh_task_next` returns the oldest open task; nothing open returns `{ found: false }`.
31
+ - `rh_task_update` takes a state and/or a `result` — a failed task should carry what was tried and what went wrong, so the record says what happened.
32
+ - `rh_task_list` sorts open first (oldest first), then done, then failed; `state` filters.
33
+
26
34
  ## Credential semantics
27
35
 
28
36
  - `rh_credential_describe` reports `configured`, `source`, `writable` — never the value.
@@ -39,9 +47,11 @@ The righthand toolkit: DSH-native tools over the harness's own services. Use thi
39
47
 
40
48
  ## Guard policy
41
49
 
42
- Rules come from the plugin config (`rules: [{ toolPrefix, mode, ask? }]`). `deny` throws before dispatch; `ask` defers to the policy function (return true to allow); `allow` passes through. Tools matching no rule are unaffected.
50
+ Rules come from the plugin config (`rules: [{ toolPrefix, mode, ask?, destructive? }]`). `deny` throws before dispatch; `ask` defers to the policy function (return true to allow); `allow` passes through. Tools matching no rule are unaffected. Prefixes match by prefix — `rh_run` also covers `rh_run_bg`.
51
+
52
+ `destructive: true` is a documentation flag for irreversible effects (deploy, delete, DNS change); it never changes enforcement, only records why a prefix is gated. `tests/permissions.golden` records every tool's derived guard facts; a guard change that moves a fact fails the test until re-baselined (`UPDATE_GOLDEN=1 pnpm test`).
43
53
 
44
54
  ## Service availability
45
55
 
46
- - **web profile**: all twelve tools register (`storageDomain` is provided by the web app rows).
47
- - **other profiles** (e.g. headless): the store family stays dormant; the other seven tools still register. The plugin never fails the boot.
56
+ - **web profile**: all sixteen tools register (`storageDomain` is provided by the web app rows).
57
+ - **other profiles** (e.g. headless): the store and task families stay dormant; the other seven tools still register. The plugin never fails the boot.
@@ -3,6 +3,14 @@
3
3
  * Demonstrates the §4.4 allow/deny/ask gate the righthand plugin uses to
4
4
  * gate high-impact deploy/invoke tools. Built on ctx.on('tools/pre-execute'),
5
5
  * not a hand-rolled wrapper.
6
+ *
7
+ * Rules are prefix-based. A rule may also declare `destructive: true` — a
8
+ * pure documentation flag saying the gated tools have irreversible effects
9
+ * (deploy, delete, DNS change): the kind of surface a tool *description*
10
+ * (attacker-controlled text) could talk the agent into. The flag does not
11
+ * change enforcement — `mode` does — it records WHY the prefix is guarded,
12
+ * and `tests/permissions.golden` records it so guard changes are never
13
+ * silent.
6
14
  * @module dsh-righthand/guard-tools
7
15
  */
8
16
 
@@ -16,25 +24,49 @@ export interface GuardRule {
16
24
  toolPrefix: string
17
25
  /** 'allow' passes through; 'deny' blocks; 'ask' defers to a policy function. */
18
26
  mode: 'allow' | 'deny' | 'ask'
27
+ /**
28
+ * Documentation flag: the gated tools have irreversible effects nobody
29
+ * asked for (a deploy, a delete, a DNS change). Declarative only —
30
+ * enforcement is the rule's `mode`. Default false.
31
+ */
32
+ destructive?: boolean
19
33
  /** Only consulted when mode is 'ask'. Return true to allow. */
20
34
  ask?: (args: unknown) => boolean
21
35
  }
22
36
 
37
+ /**
38
+ * The guard facts a tool name resolves to: the mode the first matching rule
39
+ * enforces (or 'none' when no rule matches) and whether that rule marks the
40
+ * prefix destructive. This is the SINGLE lookup both the enforcement hook
41
+ * and the permissions golden use, so the recorded facts cannot drift from
42
+ * what the gate actually does.
43
+ */
44
+ export interface GuardFacts {
45
+ mode: 'allow' | 'deny' | 'ask' | 'none'
46
+ destructive: boolean
47
+ /** The matched rule, when one matched — the enforcement hook's ask path needs it. */
48
+ rule: GuardRule | undefined
49
+ }
50
+
51
+ /** Resolve one tool name to its guard facts. Rules match in order, first wins. */
52
+ export function guardFactsFor(toolName: string, rules: readonly GuardRule[]): GuardFacts {
53
+ const rule = rules.find(r => toolName.startsWith(r.toolPrefix))
54
+ if (rule === undefined) return { mode: 'none', destructive: false, rule: undefined }
55
+ return { mode: rule.mode, destructive: rule.destructive === true, rule }
56
+ }
57
+
23
58
  /** Apply a guard over a set of tool-name prefixes. */
24
59
  export function apply(ctx: Context, config: { rules?: GuardRule[] } = {}): void {
25
60
  const rules = config.rules ?? []
26
61
 
27
62
  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') {
63
+ const facts = guardFactsFor(exec.name, rules)
64
+ if (facts.mode === 'none' || facts.mode === 'allow') return next()
65
+ if (facts.mode === 'deny') {
31
66
  throw new Error(`tool ${exec.name} is denied by righthand guard`)
32
67
  }
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
- }
68
+ const allowed = facts.rule?.ask?.(exec.arguments) ?? false
69
+ if (!allowed) throw new Error(`tool ${exec.name} requires approval (ask returned false)`)
38
70
  return next()
39
71
  }, { prepend: true })
40
- }
72
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * Modules (each individually mountable via `export * as ...`):
15
15
  * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
16
16
  * - secrets-tools: rh_credential_describe/set/unset + rh_settings_get/set
17
+ * - task-tools: rh_task_create/list/next/update/delete over ctx.storageDomain
17
18
  * - exec-tools: rh_run / rh_run_bg over ctx.subprocess + ctx.jobs
18
19
  * - guard-tools: tools/pre-execute policy (config.rules)
19
20
  * - skills: the packaged dsh-righthand skill (ctx.skills)
@@ -26,6 +27,7 @@ import * as storeTools from './store-tools.ts'
26
27
  import * as secretsTools from './secrets-tools.ts'
27
28
  import * as execTools from './exec-tools.ts'
28
29
  import * as guardTools from './guard-tools.ts'
30
+ import * as taskTools from './task-tools.ts'
29
31
  import * as righthandSkills from './skills.ts'
30
32
  import type { GuardRule } from './guard-tools.ts'
31
33
 
@@ -50,12 +52,15 @@ export function apply(ctx: Context, config: RighthandConfig = {}): void {
50
52
  ctx.plugin(storeTools)
51
53
  ctx.plugin(secretsTools)
52
54
  ctx.plugin(execTools)
55
+ ctx.plugin(taskTools)
53
56
  ctx.plugin(guardTools, { rules: config.rules ?? [] })
54
57
  ctx.plugin(righthandSkills)
55
58
  }
56
59
 
57
60
  // Individual modules stay importable for selective mounting.
58
- export { storeTools, secretsTools, execTools, guardTools, righthandSkills }
61
+ export { storeTools, secretsTools, execTools, taskTools, guardTools, righthandSkills }
59
62
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from './skills.ts'
60
63
  export type { GuardRule } from './guard-tools.ts'
64
+ export { guardFactsFor } from './guard-tools.ts'
65
+ export type { GuardFacts } from './guard-tools.ts'
61
66
 
@@ -0,0 +1,236 @@
1
+ /**
2
+ * task-tools — rh_task_* over ctx.storageDomain.
3
+ * A typed task board with a state machine: open → done | failed.
4
+ * `rh_task_next` is the queue discipline — the oldest open task is what to
5
+ * work on now. Descriptions carry the rule that a task's failure is recorded
6
+ * like an answer: a run that could not happen is news the owner needs.
7
+ * @module dsh-righthand/task-tools
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import { defineTool } from '@deepseek-ai/dsh-tools'
12
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
13
+ import type { Domain } from '@deepseek-ai/dsh-storage-domain'
14
+ import { z } from 'zod'
15
+
16
+ export const name = 'righthand-task'
17
+ export const inject = ['tools', 'storageDomain']
18
+
19
+ export type TaskState = 'open' | 'done' | 'failed'
20
+
21
+ export interface Task {
22
+ id: string
23
+ title: string
24
+ detail: string
25
+ due: string
26
+ state: TaskState
27
+ result: string
28
+ createdAt: string
29
+ updatedAt: string
30
+ }
31
+
32
+ /** Domain spec: the typed task table. */
33
+ export const taskDomain = defineDomain({
34
+ name: 'righthand_tasks',
35
+ version: 1,
36
+ tables: {
37
+ tasks: domainTable(z.object({
38
+ id: z.string(),
39
+ title: z.string(),
40
+ detail: z.string(),
41
+ due: z.string(),
42
+ state: z.enum(['open', 'done', 'failed']),
43
+ result: z.string(),
44
+ createdAt: z.string(),
45
+ updatedAt: z.string(),
46
+ })),
47
+ },
48
+ })
49
+
50
+ export type TaskDomain = Domain<typeof taskDomain>
51
+
52
+ function newId(): string {
53
+ return 't-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6)
54
+ }
55
+
56
+ /** Open the task domain and register the model-facing tools. */
57
+ export function apply(ctx: Context): void {
58
+ let domain: TaskDomain | undefined
59
+ let ready: Promise<void> | undefined
60
+
61
+ ctx.effect(() => {
62
+ ready = (async () => {
63
+ domain = await ctx.storageDomain.open(taskDomain)
64
+ })()
65
+ return () => { /* domain facility owns the domain */ }
66
+ })
67
+
68
+ const ensure = async (): Promise<TaskDomain> => {
69
+ if (domain === undefined) { await ready; }
70
+ if (domain === undefined) throw new Error('righthand tasks are not open (storageDomain not mounted)')
71
+ return domain
72
+ }
73
+
74
+ const all = async (): Promise<Task[]> => {
75
+ const d = await ensure()
76
+ return [...d.table('tasks').entries()].map(([, t]) => t)
77
+ }
78
+
79
+ ctx.tools.register(defineTool({
80
+ name: 'rh_task_create',
81
+ description: 'Add a task with a title, optional detail and due date. Returns the created task with a generated id and state open. A task the agent cannot run is still recorded — its failure is delivered like an answer.',
82
+ parameters: {
83
+ title: { type: 'string', required: true, description: 'What the task is, e.g. "deploy vision-worker".' },
84
+ detail: { type: 'string', description: 'Optional longer description of what done means.' },
85
+ due: { type: 'string', description: 'Optional due date, YYYY-MM-DD.' },
86
+ },
87
+ output: {
88
+ schema: {
89
+ type: 'object',
90
+ additionalProperties: false,
91
+ properties: {
92
+ id: { type: 'string', required: true },
93
+ title: { type: 'string', required: true },
94
+ state: { type: 'string', required: true },
95
+ },
96
+ },
97
+ render: (_args, value) => [{ type: 'text', text: 'task ' + value.id + ' created: ' + value.title + ' (open)' }],
98
+ },
99
+ async execute(args) {
100
+ const d = await ensure()
101
+ const now = new Date().toISOString()
102
+ const task: Task = {
103
+ id: newId(),
104
+ title: args.title.trim(),
105
+ detail: args.detail ?? '',
106
+ due: args.due ?? '',
107
+ state: 'open',
108
+ result: '',
109
+ createdAt: now,
110
+ updatedAt: now,
111
+ }
112
+ if (task.title === '') throw new Error('a task needs a title')
113
+ await d.table('tasks').put(task.id, task)
114
+ return { id: task.id, title: task.title, state: task.state }
115
+ },
116
+ }))
117
+ ctx.tools.register(defineTool({
118
+ name: 'rh_task_list',
119
+ description: 'List tasks, open ones first (oldest open first). Optionally filter by state.',
120
+ parameters: {
121
+ state: { type: 'string', description: 'Filter: open | done | failed. Omit for all tasks.' },
122
+ },
123
+ output: {
124
+ schema: {
125
+ type: 'array',
126
+ items: {
127
+ type: 'object',
128
+ additionalProperties: false,
129
+ properties: {
130
+ id: { type: 'string', required: true },
131
+ title: { type: 'string', required: true },
132
+ state: { type: 'string', required: true },
133
+ due: { type: 'string' },
134
+ },
135
+ },
136
+ },
137
+ render: (_args, tasks) => [{ type: 'text', text: tasks.length === 0 ? '(no tasks)' : tasks.map((t: any) => '[' + t.state + '] ' + t.title).join(' / ') }],
138
+ },
139
+ async execute(args) {
140
+ const tasks = await all()
141
+ const wanted = args.state ?? null
142
+ const filtered = wanted ? tasks.filter(t => t.state === wanted) : tasks
143
+ const rank = { open: 0, done: 1, failed: 2 } as Record<string, number>
144
+ filtered.sort((a, b) => (rank[a.state] - rank[b.state]) || a.createdAt.localeCompare(b.createdAt))
145
+ return filtered.map(t => ({ id: t.id, title: t.title, state: t.state, ...t.due ? { due: t.due } : {} }))
146
+ },
147
+ }))
148
+
149
+ ctx.tools.register(defineTool({
150
+ name: 'rh_task_next',
151
+ description: 'The next open task — what to work on now. Oldest open task first. Returns found: false when nothing is open.',
152
+ parameters: {},
153
+ output: {
154
+ schema: {
155
+ type: 'object',
156
+ additionalProperties: false,
157
+ properties: {
158
+ found: { type: 'boolean', required: true },
159
+ id: { type: 'string' },
160
+ title: { type: 'string' },
161
+ detail: { type: 'string' },
162
+ due: { type: 'string' },
163
+ },
164
+ },
165
+ render: (_args, value) => [{ type: 'text', text: value.found ? 'next: ' + value.title : '(nothing open)' }],
166
+ },
167
+ async execute() {
168
+ const tasks = await all()
169
+ const open = tasks.filter(t => t.state === 'open').sort((a, b) => a.createdAt.localeCompare(b.createdAt))
170
+ const t = open[0]
171
+ if (t === undefined) return { found: false }
172
+ return { found: true, id: t.id, title: t.title, ...t.detail ? { detail: t.detail } : {}, ...t.due ? { due: t.due } : {} }
173
+ },
174
+ }))
175
+ ctx.tools.register(defineTool({
176
+ name: 'rh_task_update',
177
+ description: 'Change a task: its state (open | done | failed) and/or its result. A failed task should carry its result — what was tried and what went wrong — so the record says what happened, not just that something did.',
178
+ parameters: {
179
+ id: { type: 'string', required: true, description: 'The task id from rh_task_create or rh_task_list.' },
180
+ state: { type: 'string', description: 'New state: open | done | failed.' },
181
+ result: { type: 'string', description: 'What happened — the outcome, or the failure reason.' },
182
+ },
183
+ output: {
184
+ schema: {
185
+ type: 'object',
186
+ additionalProperties: false,
187
+ properties: {
188
+ found: { type: 'boolean', required: true },
189
+ id: { type: 'string', required: true },
190
+ state: { type: 'string', required: true },
191
+ },
192
+ },
193
+ render: (_args, value) => [{ type: 'text', text: value.found ? 'task ' + value.id + ' -> ' + value.state : 'task ' + value.id + ' not found' }],
194
+ },
195
+ async execute(args) {
196
+ const d = await ensure()
197
+ const task = d.table('tasks').get(args.id)
198
+ if (task === undefined) return { found: false, id: args.id, state: 'open' }
199
+ const next: Task = {
200
+ ...task,
201
+ ...args.state !== undefined ? { state: args.state as TaskState } : {},
202
+ ...args.result !== undefined ? { result: args.result } : {},
203
+ updatedAt: new Date().toISOString(),
204
+ }
205
+ await d.table('tasks').put(args.id, next)
206
+ return { found: true, id: args.id, state: next.state }
207
+ },
208
+ }))
209
+
210
+ ctx.tools.register(defineTool({
211
+ name: 'rh_task_delete',
212
+ description: 'Remove a task by id. Returns whether it existed.',
213
+ parameters: {
214
+ id: { type: 'string', required: true, description: 'The task id to remove.' },
215
+ },
216
+ output: {
217
+ schema: {
218
+ type: 'object',
219
+ additionalProperties: false,
220
+ properties: {
221
+ id: { type: 'string', required: true },
222
+ existed: { type: 'boolean', required: true },
223
+ },
224
+ },
225
+ render: (_args, value) => [{ type: 'text', text: 'deleted ' + value.id + ': ' + value.existed }],
226
+ },
227
+ async execute(args) {
228
+ const d = await ensure()
229
+ const existed = await d.table('tasks').delete(args.id)
230
+ return { id: args.id, existed }
231
+ },
232
+ }))
233
+ }
234
+
235
+
236
+