@cogineai/dearharness 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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/cli.js +259 -0
  4. package/dist/config.js +21 -0
  5. package/dist/daemon/server.js +225 -0
  6. package/dist/extensions/builtin/policy-instructions.js +19 -0
  7. package/dist/extensions/loader.js +58 -0
  8. package/dist/extensions/types.js +1 -0
  9. package/dist/harness/install-applicator.js +126 -0
  10. package/dist/harness/install-apply.js +156 -0
  11. package/dist/harness/install-plan.js +154 -0
  12. package/dist/harness/install-runner.js +178 -0
  13. package/dist/harness/install-verification.js +117 -0
  14. package/dist/harness/lockfile.js +83 -0
  15. package/dist/harness/manifest.js +491 -0
  16. package/dist/harness/source.js +224 -0
  17. package/dist/harness/transaction.js +77 -0
  18. package/dist/harness/workspace.js +61 -0
  19. package/dist/index.js +9 -0
  20. package/dist/instructions/builder.js +33 -0
  21. package/dist/instructions/types.js +1 -0
  22. package/dist/model/config.js +100 -0
  23. package/dist/model/http.js +128 -0
  24. package/dist/model/index.js +22 -0
  25. package/dist/model/openrouter.js +9 -0
  26. package/dist/model/providers/anthropic.js +104 -0
  27. package/dist/model/providers/ollama-discovery.js +32 -0
  28. package/dist/model/providers/ollama.js +70 -0
  29. package/dist/model/providers/openai-compatible.js +118 -0
  30. package/dist/model/providers/openai.js +4 -0
  31. package/dist/model/providers/openrouter.js +79 -0
  32. package/dist/model/registry.js +108 -0
  33. package/dist/model/types.js +1 -0
  34. package/dist/policy/engine.js +30 -0
  35. package/dist/policy/types.js +1 -0
  36. package/dist/prompt/system.js +30 -0
  37. package/dist/protocol/actions.js +88 -0
  38. package/dist/runtime/assembly.js +54 -0
  39. package/dist/runtime/events.js +1 -0
  40. package/dist/runtime/hooks.js +13 -0
  41. package/dist/runtime/runner.js +193 -0
  42. package/dist/session/store.js +198 -0
  43. package/dist/session/types.js +1 -0
  44. package/dist/skills/loader.js +51 -0
  45. package/dist/skills/types.js +1 -0
  46. package/dist/tools/bash.js +71 -0
  47. package/dist/tools/edit.js +61 -0
  48. package/dist/tools/find.js +67 -0
  49. package/dist/tools/grep.js +88 -0
  50. package/dist/tools/ls.js +37 -0
  51. package/dist/tools/path.js +35 -0
  52. package/dist/tools/read.js +40 -0
  53. package/dist/tools/registry.js +18 -0
  54. package/dist/tools/types.js +1 -0
  55. package/dist/workspace/config.js +72 -0
  56. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cogine AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # DearHarness
2
+
3
+ DearHarness is an OpenClaw Harness operator CLI and instance-side daemon prototype.
4
+
5
+ It is derived from `cogine-ai/cliq-agent`, but it is not a generic coding agent. Cliq provides useful runtime substrate ideas; DearHarness focuses on Harness inspection, validation, installation, maintenance, packing, publishing, and platform-callable daemon tasks.
6
+
7
+ ## Current Status
8
+
9
+ This repository is a standalone prototype. The main DearClaw platform repository may later consume or import stable pieces from it.
10
+
11
+ V0 scope:
12
+
13
+ - local CLI identity
14
+ - OpenClaw workspace inspection
15
+ - install-lock status reading
16
+ - local Harness manifest validation
17
+ - minimal private daemon task API
18
+
19
+ V1 work has started with remote `.zip` and `.tar.gz` bundle source staging, dry-run install planning, conflict reporting, guarded file apply, and post-apply verification. DearHarness still does not claim full OpenClaw config mutation, publication, package signing, dependency solving, or OpenClaw plugin installation.
20
+
21
+ ## Quick Start
22
+
23
+ Requirements: Node.js 20 or newer.
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build
28
+ node dist/index.js help
29
+ ```
30
+
31
+ Inspect an OpenClaw-like workspace:
32
+
33
+ ```bash
34
+ node dist/index.js inspect --target-workspace ~/.openclaw/workspace
35
+ ```
36
+
37
+ Read local install status:
38
+
39
+ ```bash
40
+ node dist/index.js status --target-workspace ~/.openclaw/workspace
41
+ ```
42
+
43
+ Validate a Harness source directory:
44
+
45
+ ```bash
46
+ node dist/index.js validate ./my-harness
47
+ ```
48
+
49
+ Download and validate a remote Harness bundle without applying changes. The install runner accepts either a `.zip` or `.tar.gz` archive URL:
50
+
51
+ ```bash
52
+ node dist/index.js install https://example.com/my-harness.zip --target-workspace ~/.openclaw/workspace --dry-run
53
+ node dist/index.js install https://example.com/my-harness.tar.gz --target-workspace ~/.openclaw/workspace --dry-run
54
+ ```
55
+
56
+ Apply a conflict-free plan explicitly:
57
+
58
+ ```bash
59
+ node dist/index.js install https://example.com/my-harness.zip --target-workspace ~/.openclaw/workspace --apply
60
+ node dist/index.js install https://example.com/my-harness.tar.gz --target-workspace ~/.openclaw/workspace --apply
61
+ ```
62
+
63
+ Run the V0 daemon:
64
+
65
+ ```bash
66
+ DEARHARNESS_DAEMON_TOKEN=dev-token node dist/index.js daemon --host 127.0.0.1 --port 18790
67
+ ```
68
+
69
+ Submit a V0 daemon task:
70
+
71
+ ```bash
72
+ curl -sS \
73
+ -H 'authorization: Bearer dev-token' \
74
+ -H 'content-type: application/json' \
75
+ -d '{"type":"inspect_workspace","targetWorkspace":"/workspace/openclaw"}' \
76
+ http://127.0.0.1:18790/tasks
77
+ ```
78
+
79
+ Submit a guarded install task to the daemon. The `source` field accepts either a `.zip` or `.tar.gz` archive URL:
80
+
81
+ ```bash
82
+ curl -sS \
83
+ -H 'authorization: Bearer dev-token' \
84
+ -H 'content-type: application/json' \
85
+ -d '{"type":"install_harness","source":"https://example.com/my-harness.zip","targetWorkspace":"/workspace/openclaw","mode":"dry_run"}' \
86
+ http://127.0.0.1:18790/tasks
87
+
88
+ curl -sS \
89
+ -H 'authorization: Bearer dev-token' \
90
+ -H 'content-type: application/json' \
91
+ -d '{"type":"install_harness","source":"https://example.com/my-harness.tar.gz","targetWorkspace":"/workspace/openclaw","mode":"dry_run"}' \
92
+ http://127.0.0.1:18790/tasks
93
+ ```
94
+
95
+ ## Repository Guide
96
+
97
+ Read these first:
98
+
99
+ - `AGENTS.md`
100
+ - `docs/project.md`
101
+ - `docs/architecture.md`
102
+ - `docs/roadmap.md`
103
+ - `docs/superpowers/plans/2026-04-30-dearharness-v0.md`
104
+
105
+ ## Relationship To DearClaw
106
+
107
+ DearClaw owns platform policy, auth, catalog, publish review, audit, billing, and user-facing status.
108
+
109
+ DearHarness owns local execution near OpenClaw:
110
+
111
+ - inspect workspace/config
112
+ - validate packages
113
+ - apply deterministic install and maintenance tools
114
+ - expose a private daemon for platform task execution
115
+
116
+ ## Relationship To Cliq
117
+
118
+ The `cliq-upstream` git remote may remain pointed at `https://github.com/cogine-ai/cliq-agent.git` for reference and selective runtime reuse. It should not be used as the DearHarness publishing remote.
119
+
120
+ If DearHarness needs a generally useful runtime capability, prefer opening an upstream Cliq issue instead of permanently hiding the change in this fork.
121
+
122
+ Extensions are intentionally limited to hooks and instruction contributions. They do not register new model-callable top-level actions.
123
+
124
+ ## V0 Task Types
125
+
126
+ The daemon currently accepts synchronous task requests:
127
+
128
+ - `inspect_workspace`: `{ "type": "inspect_workspace", "targetWorkspace": "/path" }`
129
+ - `read_status`: `{ "type": "read_status", "targetWorkspace": "/path" }`
130
+ - `validate_harness`: `{ "type": "validate_harness", "sourcePath": "/path" }`
131
+ - `install_harness`: `{ "type": "install_harness", "source": "https://example.com/my-harness.zip", "targetWorkspace": "/path", "mode": "dry_run" }`. The `source` field accepts an http(s) `.zip` or `.tar.gz` URL, and `mode` is either `dry_run` or `apply`.
132
+
133
+ These are deliberately narrow. Future agentic maintenance should choose from deterministic tools instead of relying on unrestricted shell execution.
134
+
135
+ ## Contributing
136
+
137
+ Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
138
+
139
+ ## License
140
+
141
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,259 @@
1
+ import { startDaemon } from './daemon/server.js';
2
+ import { executeHarnessInstall } from './harness/install-runner.js';
3
+ import { readInstallLockfile } from './harness/lockfile.js';
4
+ import { validateHarnessSource } from './harness/manifest.js';
5
+ import { inspectOpenClawWorkspace } from './harness/workspace.js';
6
+ export class ReportedCliError extends Error {
7
+ constructor(error) {
8
+ super(error instanceof Error ? error.message : String(error), { cause: error });
9
+ this.name = 'ReportedCliError';
10
+ }
11
+ }
12
+ export function isReportedCliError(error) {
13
+ return error instanceof ReportedCliError;
14
+ }
15
+ export function renderUnhandledError(error) {
16
+ if (isReportedCliError(error)) {
17
+ return null;
18
+ }
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ function readFlagValue(raw, index, flag) {
22
+ const value = raw[index + 1];
23
+ if (value === undefined || value === '' || value.startsWith('-')) {
24
+ throw new Error(`Missing value for ${flag}`);
25
+ }
26
+ return value;
27
+ }
28
+ function takeStringOption(raw, index, flag) {
29
+ const token = raw[index];
30
+ if (token.startsWith(`${flag}=`)) {
31
+ const value = token.slice(flag.length + 1);
32
+ if (!value)
33
+ throw new Error(`Missing value for ${flag}`);
34
+ return { value, nextIndex: index };
35
+ }
36
+ if (token === flag) {
37
+ return { value: readFlagValue(raw, index, flag), nextIndex: index + 1 };
38
+ }
39
+ return null;
40
+ }
41
+ function readTargetWorkspace(raw) {
42
+ let targetWorkspace = null;
43
+ const passthrough = [];
44
+ for (let i = 0; i < raw.length; i += 1) {
45
+ const target = takeStringOption(raw, i, '--target-workspace') ?? takeStringOption(raw, i, '--workspace');
46
+ if (target) {
47
+ targetWorkspace = target.value;
48
+ i = target.nextIndex;
49
+ continue;
50
+ }
51
+ passthrough.push(raw[i]);
52
+ }
53
+ if (!targetWorkspace) {
54
+ throw new Error('Missing value for --target-workspace');
55
+ }
56
+ if (passthrough.length > 0) {
57
+ throw new Error(`Unexpected argument for workspace command: ${passthrough[0]}`);
58
+ }
59
+ return targetWorkspace;
60
+ }
61
+ function readValidateSource(raw) {
62
+ let sourcePath = null;
63
+ const passthrough = [];
64
+ for (let i = 0; i < raw.length; i += 1) {
65
+ const source = takeStringOption(raw, i, '--source');
66
+ if (source) {
67
+ sourcePath = source.value;
68
+ i = source.nextIndex;
69
+ continue;
70
+ }
71
+ passthrough.push(raw[i]);
72
+ }
73
+ if (!sourcePath) {
74
+ sourcePath = passthrough.shift() ?? null;
75
+ }
76
+ if (!sourcePath) {
77
+ throw new Error('Missing harness source path');
78
+ }
79
+ if (passthrough.length > 0) {
80
+ throw new Error(`Unexpected argument for validate command: ${passthrough[0]}`);
81
+ }
82
+ return sourcePath;
83
+ }
84
+ function readInstallOptions(raw) {
85
+ let source = null;
86
+ let targetWorkspace = null;
87
+ let dryRun = false;
88
+ let apply = false;
89
+ let expectedSha256;
90
+ const passthrough = [];
91
+ for (let i = 0; i < raw.length; i += 1) {
92
+ const target = takeStringOption(raw, i, '--target-workspace') ?? takeStringOption(raw, i, '--workspace');
93
+ if (target) {
94
+ targetWorkspace = target.value;
95
+ i = target.nextIndex;
96
+ continue;
97
+ }
98
+ const sha256 = takeStringOption(raw, i, '--sha256');
99
+ if (sha256) {
100
+ expectedSha256 = sha256.value;
101
+ i = sha256.nextIndex;
102
+ continue;
103
+ }
104
+ if (raw[i] === '--dry-run') {
105
+ dryRun = true;
106
+ continue;
107
+ }
108
+ if (raw[i] === '--apply') {
109
+ apply = true;
110
+ continue;
111
+ }
112
+ passthrough.push(raw[i]);
113
+ }
114
+ source = passthrough.shift() ?? null;
115
+ if (!source) {
116
+ throw new Error('Missing harness source URL');
117
+ }
118
+ if (!targetWorkspace) {
119
+ throw new Error('Missing value for --target-workspace');
120
+ }
121
+ if (passthrough.length > 0) {
122
+ throw new Error(`Unexpected argument for install command: ${passthrough[0]}`);
123
+ }
124
+ if (dryRun && apply) {
125
+ throw new Error('install accepts either --dry-run or --apply, not both');
126
+ }
127
+ return {
128
+ source,
129
+ targetWorkspace,
130
+ dryRun,
131
+ ...(apply ? { apply: true } : {}),
132
+ ...(expectedSha256 ? { expectedSha256 } : {}),
133
+ };
134
+ }
135
+ function readDaemonOptions(raw) {
136
+ let host = '127.0.0.1';
137
+ let port = 18790;
138
+ let token = process.env.DEARHARNESS_DAEMON_TOKEN ?? null;
139
+ const passthrough = [];
140
+ for (let i = 0; i < raw.length; i += 1) {
141
+ const hostOption = takeStringOption(raw, i, '--host');
142
+ if (hostOption) {
143
+ host = hostOption.value;
144
+ i = hostOption.nextIndex;
145
+ continue;
146
+ }
147
+ const portOption = takeStringOption(raw, i, '--port');
148
+ if (portOption) {
149
+ const parsed = Number(portOption.value);
150
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
151
+ throw new Error('--port must be an integer between 0 and 65535');
152
+ }
153
+ port = parsed;
154
+ i = portOption.nextIndex;
155
+ continue;
156
+ }
157
+ const tokenOption = takeStringOption(raw, i, '--token');
158
+ if (tokenOption) {
159
+ token = tokenOption.value;
160
+ i = tokenOption.nextIndex;
161
+ continue;
162
+ }
163
+ passthrough.push(raw[i]);
164
+ }
165
+ if (passthrough.length > 0) {
166
+ throw new Error(`Unexpected argument for daemon command: ${passthrough[0]}`);
167
+ }
168
+ return { host, port, token };
169
+ }
170
+ export function parseArgs(argv) {
171
+ const raw = argv.slice(2);
172
+ const command = raw.shift();
173
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
174
+ return { command: 'help' };
175
+ }
176
+ if (command === 'inspect') {
177
+ return { command, targetWorkspace: readTargetWorkspace(raw) };
178
+ }
179
+ if (command === 'status') {
180
+ return { command, targetWorkspace: readTargetWorkspace(raw) };
181
+ }
182
+ if (command === 'validate') {
183
+ return { command, sourcePath: readValidateSource(raw) };
184
+ }
185
+ if (command === 'install') {
186
+ return { command, ...readInstallOptions(raw) };
187
+ }
188
+ if (command === 'daemon') {
189
+ return { command, ...readDaemonOptions(raw) };
190
+ }
191
+ throw new Error(`Unknown dearharness command: ${command}`);
192
+ }
193
+ export function formatHelp() {
194
+ return `dearharness - OpenClaw Harness operator
195
+
196
+ Usage:
197
+ dearharness inspect --target-workspace PATH
198
+ dearharness status --target-workspace PATH
199
+ dearharness validate PATH
200
+ dearharness install URL --target-workspace PATH --dry-run
201
+ dearharness install URL --target-workspace PATH --apply
202
+ dearharness daemon [--host HOST] [--port PORT] [--token TOKEN]
203
+ dearharness help
204
+
205
+ Commands:
206
+ inspect Describe an OpenClaw workspace without modifying it
207
+ status Read the DearHarness install lockfile
208
+ validate Validate a harness source directory and manifest
209
+ install Plan or apply a remote harness bundle through guarded staging
210
+ daemon Serve private-network task requests for a DearClaw instance
211
+
212
+ Environment:
213
+ DEARHARNESS_DAEMON_TOKEN Default bearer token for daemon mode
214
+ `;
215
+ }
216
+ export function printHelp(io = {}) {
217
+ const write = io.stdout ?? ((value) => process.stdout.write(value));
218
+ write(formatHelp());
219
+ }
220
+ function writeJson(io, value) {
221
+ const write = io.stdout ?? ((chunk) => process.stdout.write(chunk));
222
+ write(`${JSON.stringify(value, null, 2)}\n`);
223
+ }
224
+ export async function runCli(argv, io = {}, dependencies = {}) {
225
+ const args = parseArgs(argv);
226
+ if (args.command === 'help') {
227
+ printHelp(io);
228
+ return;
229
+ }
230
+ if (args.command === 'inspect') {
231
+ writeJson(io, await inspectOpenClawWorkspace(args.targetWorkspace));
232
+ return;
233
+ }
234
+ if (args.command === 'status') {
235
+ writeJson(io, await readInstallLockfile(args.targetWorkspace));
236
+ return;
237
+ }
238
+ if (args.command === 'validate') {
239
+ writeJson(io, await validateHarnessSource(args.sourcePath));
240
+ return;
241
+ }
242
+ if (args.command === 'install') {
243
+ if (!args.dryRun && !args.apply) {
244
+ throw new Error('install requires either --dry-run or --apply');
245
+ }
246
+ writeJson(io, await executeHarnessInstall({
247
+ source: args.source,
248
+ targetWorkspace: args.targetWorkspace,
249
+ mode: args.apply ? 'apply' : 'dry_run',
250
+ ...(args.expectedSha256 ? { expectedSha256: args.expectedSha256 } : {}),
251
+ ...(dependencies.materializeHarnessSource ? { materializeHarnessSource: dependencies.materializeHarnessSource } : {}),
252
+ }));
253
+ return;
254
+ }
255
+ if (args.command === 'daemon') {
256
+ const daemon = await startDaemon({ host: args.host, port: args.port, token: args.token });
257
+ writeJson(io, { ok: true, service: 'dearharness-daemon', host: daemon.host, port: daemon.port });
258
+ }
259
+ }
package/dist/config.js ADDED
@@ -0,0 +1,21 @@
1
+ export const MODEL = 'anthropic/claude-sonnet-4.6';
2
+ export const DEFAULT_MODEL_PROVIDER = 'openrouter';
3
+ export const DEFAULT_MODEL_BASE_URL = 'https://openrouter.ai/api/v1';
4
+ export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434';
5
+ export const OLLAMA_DEFAULT_MODEL_HINT = 'qwen3:4b';
6
+ export const OLLAMA_DISCOVERY_TIMEOUT_MS = 2_000;
7
+ export const APP_DIR = '.cliq';
8
+ export const SESSION_FILE = 'session.json';
9
+ export const MAX_LOOPS = 24;
10
+ export const MAX_OUTPUT = 12_000;
11
+ export const BASH_TIMEOUT_MS = 60_000;
12
+ export const MODEL_TIMEOUT_MS = 20_000;
13
+ export const OPENROUTER_TIMEOUT_MS = MODEL_TIMEOUT_MS;
14
+ export const SESSION_VERSION = 4;
15
+ export const DEFAULT_POLICY_MODE = 'auto';
16
+ export const READ_MAX_BYTES = 8_000;
17
+ export const LIST_MAX_ENTRIES = 200;
18
+ export const FIND_MAX_RESULTS = 200;
19
+ export const FIND_MAX_DEPTH = 12;
20
+ export const GREP_MAX_MATCHES = 200;
21
+ export const GREP_MAX_FILE_BYTES = 64_000;
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import http from 'node:http';
3
+ import { executeHarnessInstall } from '../harness/install-runner.js';
4
+ import { readInstallLockfile } from '../harness/lockfile.js';
5
+ import { validateHarnessSource } from '../harness/manifest.js';
6
+ import { inspectOpenClawWorkspace } from '../harness/workspace.js';
7
+ function isRecord(value) {
8
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+ function jsonResponse(value, status = 200) {
11
+ return new Response(JSON.stringify(value), {
12
+ status,
13
+ headers: { 'content-type': 'application/json; charset=utf-8' },
14
+ });
15
+ }
16
+ function requireBearerToken(request, token) {
17
+ if (!token)
18
+ return null;
19
+ if (request.headers.get('authorization') === `Bearer ${token}`)
20
+ return null;
21
+ return jsonResponse({ error: 'unauthorized' }, 401);
22
+ }
23
+ function readNonEmptyString(record, field) {
24
+ const value = record[field];
25
+ return typeof value === 'string' && value.length > 0 ? value : null;
26
+ }
27
+ function parseTaskRequest(value) {
28
+ if (!isRecord(value))
29
+ return { task: null, error: 'task request must be a JSON object' };
30
+ if (value.type === 'inspect_workspace') {
31
+ const targetWorkspace = readNonEmptyString(value, 'targetWorkspace');
32
+ if (!targetWorkspace) {
33
+ return { task: null, error: 'inspect_workspace targetWorkspace must be a non-empty string' };
34
+ }
35
+ return { task: { type: 'inspect_workspace', targetWorkspace }, error: null };
36
+ }
37
+ if (value.type === 'read_status') {
38
+ const targetWorkspace = readNonEmptyString(value, 'targetWorkspace');
39
+ if (!targetWorkspace) {
40
+ return { task: null, error: 'read_status targetWorkspace must be a non-empty string' };
41
+ }
42
+ return { task: { type: 'read_status', targetWorkspace }, error: null };
43
+ }
44
+ if (value.type === 'validate_harness') {
45
+ const sourcePath = readNonEmptyString(value, 'sourcePath');
46
+ if (!sourcePath) {
47
+ return { task: null, error: 'validate_harness sourcePath must be a non-empty string' };
48
+ }
49
+ return { task: { type: 'validate_harness', sourcePath }, error: null };
50
+ }
51
+ if (value.type === 'install_harness') {
52
+ const source = readNonEmptyString(value, 'source');
53
+ if (!source) {
54
+ return { task: null, error: 'install_harness source must be a non-empty string' };
55
+ }
56
+ const targetWorkspace = readNonEmptyString(value, 'targetWorkspace');
57
+ if (!targetWorkspace) {
58
+ return { task: null, error: 'install_harness targetWorkspace must be a non-empty string' };
59
+ }
60
+ if (value.mode !== 'dry_run' && value.mode !== 'apply') {
61
+ return { task: null, error: 'install_harness mode must be dry_run or apply' };
62
+ }
63
+ if (value.expectedSha256 !== undefined && (typeof value.expectedSha256 !== 'string' || value.expectedSha256.length === 0)) {
64
+ return { task: null, error: 'install_harness expectedSha256 must be a non-empty string when present' };
65
+ }
66
+ return {
67
+ task: {
68
+ type: 'install_harness',
69
+ source,
70
+ targetWorkspace,
71
+ mode: value.mode,
72
+ ...(value.expectedSha256 ? { expectedSha256: value.expectedSha256 } : {}),
73
+ },
74
+ error: null,
75
+ };
76
+ }
77
+ return { task: null, error: 'unknown task type' };
78
+ }
79
+ async function runTask(task) {
80
+ if (task.type === 'inspect_workspace') {
81
+ return inspectOpenClawWorkspace(task.targetWorkspace);
82
+ }
83
+ if (task.type === 'read_status') {
84
+ return readInstallLockfile(task.targetWorkspace);
85
+ }
86
+ if (task.type === 'install_harness') {
87
+ return executeHarnessInstall({
88
+ source: task.source,
89
+ targetWorkspace: task.targetWorkspace,
90
+ mode: task.mode,
91
+ ...(task.expectedSha256 ? { expectedSha256: task.expectedSha256 } : {}),
92
+ });
93
+ }
94
+ return validateHarnessSource(task.sourcePath);
95
+ }
96
+ async function parseJsonBody(request) {
97
+ try {
98
+ return { value: await request.json(), error: null };
99
+ }
100
+ catch (error) {
101
+ return {
102
+ value: null,
103
+ error: `invalid JSON body: ${error instanceof Error ? error.message : String(error)}`,
104
+ };
105
+ }
106
+ }
107
+ function nodeHeadersToHeaders(request) {
108
+ const headers = new Headers();
109
+ for (const [name, value] of Object.entries(request.headers)) {
110
+ if (value === undefined)
111
+ continue;
112
+ if (Array.isArray(value)) {
113
+ for (const item of value)
114
+ headers.append(name, item);
115
+ continue;
116
+ }
117
+ headers.set(name, value);
118
+ }
119
+ return headers;
120
+ }
121
+ async function nodeRequestToWebRequest(request) {
122
+ const chunks = [];
123
+ for await (const chunk of request) {
124
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
125
+ }
126
+ const method = request.method ?? 'GET';
127
+ const headers = nodeHeadersToHeaders(request);
128
+ const host = headers.get('host') ?? '127.0.0.1';
129
+ const body = method === 'GET' || method === 'HEAD' || chunks.length === 0 ? undefined : Buffer.concat(chunks);
130
+ return new Request(`http://${host}${request.url ?? '/'}`, { method, headers, body });
131
+ }
132
+ async function writeWebResponse(response, target) {
133
+ target.statusCode = response.status;
134
+ response.headers.forEach((value, name) => target.setHeader(name, value));
135
+ target.end(Buffer.from(await response.arrayBuffer()));
136
+ }
137
+ export function createDaemonServer(options = {}) {
138
+ const tasks = new Map();
139
+ return {
140
+ async handleRequest(request) {
141
+ const url = new URL(request.url);
142
+ if (request.method === 'GET' && url.pathname === '/healthz') {
143
+ return jsonResponse({ ok: true, service: 'dearharness-daemon' });
144
+ }
145
+ if (url.pathname === '/tasks' || url.pathname.startsWith('/tasks/')) {
146
+ const authResponse = requireBearerToken(request, options.token);
147
+ if (authResponse)
148
+ return authResponse;
149
+ }
150
+ if (request.method === 'POST' && url.pathname === '/tasks') {
151
+ const parsedBody = await parseJsonBody(request);
152
+ if (parsedBody.error)
153
+ return jsonResponse({ error: parsedBody.error }, 400);
154
+ const parsedTask = parseTaskRequest(parsedBody.value);
155
+ if (!parsedTask.task)
156
+ return jsonResponse({ error: parsedTask.error }, 400);
157
+ const createdAt = new Date().toISOString();
158
+ const record = {
159
+ id: randomUUID(),
160
+ status: 'succeeded',
161
+ request: parsedTask.task,
162
+ result: null,
163
+ error: null,
164
+ createdAt,
165
+ finishedAt: createdAt,
166
+ };
167
+ try {
168
+ record.result = await runTask(parsedTask.task);
169
+ record.finishedAt = new Date().toISOString();
170
+ }
171
+ catch (error) {
172
+ record.status = 'failed';
173
+ record.error = error instanceof Error ? error.message : String(error);
174
+ record.finishedAt = new Date().toISOString();
175
+ }
176
+ tasks.set(record.id, record);
177
+ return jsonResponse(record, record.status === 'succeeded' ? 200 : 500);
178
+ }
179
+ if (request.method === 'GET' && url.pathname.startsWith('/tasks/')) {
180
+ const id = decodeURIComponent(url.pathname.slice('/tasks/'.length));
181
+ const record = tasks.get(id);
182
+ if (!record)
183
+ return jsonResponse({ error: 'task not found' }, 404);
184
+ return jsonResponse(record);
185
+ }
186
+ return jsonResponse({ error: 'not found' }, 404);
187
+ },
188
+ };
189
+ }
190
+ export async function startDaemon(options = {}) {
191
+ const host = options.host ?? '127.0.0.1';
192
+ const daemon = createDaemonServer(options);
193
+ const server = http.createServer((request, response) => {
194
+ nodeRequestToWebRequest(request)
195
+ .then((webRequest) => daemon.handleRequest(webRequest))
196
+ .then((webResponse) => writeWebResponse(webResponse, response))
197
+ .catch((error) => writeWebResponse(jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500), response));
198
+ });
199
+ await new Promise((resolve, reject) => {
200
+ const onError = (error) => {
201
+ server.off('listening', onListening);
202
+ reject(error);
203
+ };
204
+ const onListening = () => {
205
+ server.off('error', onError);
206
+ resolve();
207
+ };
208
+ server.once('error', onError);
209
+ server.once('listening', onListening);
210
+ server.listen(options.port ?? 18790, host);
211
+ });
212
+ const address = server.address();
213
+ return {
214
+ host,
215
+ port: address.port,
216
+ close: () => new Promise((resolve, reject) => {
217
+ server.close((error) => {
218
+ if (error)
219
+ reject(error);
220
+ else
221
+ resolve();
222
+ });
223
+ }),
224
+ };
225
+ }
@@ -0,0 +1,19 @@
1
+ export const policyInstructionsExtension = {
2
+ name: 'policy-instructions',
3
+ instructionSources: [
4
+ async ({ policyMode }) => {
5
+ if (policyMode === 'auto') {
6
+ return [];
7
+ }
8
+ return [
9
+ {
10
+ role: 'system',
11
+ layer: 'extension',
12
+ source: 'policy-instructions',
13
+ content: `Current policy mode is ${policyMode}. Plan actions that can succeed under this mode and explain when a write or exec step would be blocked.`
14
+ }
15
+ ];
16
+ }
17
+ ],
18
+ hooks: []
19
+ };