@goodandready/dsh-goal 0.2.8 → 0.2.9

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.
@@ -225,6 +225,7 @@ export class GoalEngine {
225
225
  title: cleanTitle,
226
226
  description: options.description?.trim() || '',
227
227
  lang: options.lang || detectLanguage(cleanTitle),
228
+ cwd: options.cwd ? String(options.cwd).trim() : null,
228
229
  state: GoalState.RUNNING,
229
230
  startedAt: now,
230
231
  pausedAt: null,
package/lib/routes.js CHANGED
@@ -3,16 +3,82 @@ import path from 'node:path';
3
3
  import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
4
4
  import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
5
5
 
6
- function getSafeCwd(requestedCwd) {
7
- if (!requestedCwd || typeof requestedCwd !== 'string') {
8
- return process.cwd();
6
+ export function isLoopback(value) {
7
+ if (!value) return false;
8
+ const address = String(value).toLowerCase().replace(/^\[|\]$/g, '');
9
+ return Boolean(
10
+ address === 'localhost' ||
11
+ address === 'localhost.' ||
12
+ address === '::1' ||
13
+ address.startsWith('127.') ||
14
+ address.startsWith('::ffff:127.')
15
+ );
16
+ }
17
+
18
+ export function isTrustedCaller(req) {
19
+ if (!req) return false;
20
+ const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
21
+ if (isLoopback(remote)) return true;
22
+
23
+ const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase();
24
+ if (secSite === 'same-origin' || secSite === 'same-site' || secSite === 'none') return true;
25
+ if (secSite === 'cross-site') return false;
26
+
27
+ const host = String(req.headers?.host || '').toLowerCase();
28
+ const origin = String(req.headers?.origin || '').trim().toLowerCase();
29
+ if (origin) {
30
+ try {
31
+ const u = new URL(origin);
32
+ if (u.host === host) return true;
33
+ } catch (err) { /* invalid url */ }
34
+ return false;
35
+ }
36
+
37
+ const referer = String(req.headers?.referer || '').trim().toLowerCase();
38
+ if (referer) {
39
+ try {
40
+ const u = new URL(referer);
41
+ if (u.host === host) return true;
42
+ } catch (err) { /* invalid url */ }
43
+ return false;
9
44
  }
45
+
46
+ // Fallback for tests or local CLI without socket where host header targets loopback
47
+ if (!remote && host) {
48
+ const hostName = host.split(':')[0];
49
+ if (isLoopback(hostName)) {
50
+ return true;
51
+ }
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ export function isPathInsideOrEqual(parent, child) {
58
+ if (!parent || !child) return false;
59
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
60
+ return !rel || (!rel.startsWith('..') && !path.isAbsolute(rel));
61
+ }
62
+
63
+ export function getSafeCwd(requestedCwd, allowedRoots = []) {
64
+ const defaultRoot = allowedRoots[0] || process.cwd();
65
+ if (!requestedCwd || typeof requestedCwd !== 'string' || !requestedCwd.trim()) {
66
+ return defaultRoot;
67
+ }
68
+
10
69
  try {
11
70
  const resolved = path.resolve(requestedCwd.trim());
12
71
  const st = fs.statSync(resolved);
13
72
  if (!st.isDirectory()) {
14
73
  return null;
15
74
  }
75
+
76
+ const roots = allowedRoots.length > 0 ? allowedRoots : [process.cwd()];
77
+ const isAllowed = roots.some((root) => isPathInsideOrEqual(root, resolved));
78
+ if (!isAllowed) {
79
+ return null;
80
+ }
81
+
16
82
  return resolved;
17
83
  } catch (err) {
18
84
  return null;
@@ -144,8 +210,8 @@ export function registerRoutes(ctx, {
144
210
  return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
145
211
  }
146
212
 
147
- const origin = req.headers.origin;
148
- const host = req.headers.host;
213
+ const origin = req.headers?.origin;
214
+ const host = req.headers?.host;
149
215
  if (origin && host) {
150
216
  try {
151
217
  const originHost = new URL(origin).host;
@@ -159,6 +225,11 @@ export function registerRoutes(ctx, {
159
225
  }
160
226
  }
161
227
 
228
+ if (!isTrustedCaller(req)) {
229
+ res.statusCode = 403;
230
+ return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
231
+ }
232
+
162
233
  let body = '';
163
234
  let bodySize = 0;
164
235
  const MAX_PAYLOAD_BYTES = 256 * 1024;
@@ -196,6 +267,7 @@ export function registerRoutes(ctx, {
196
267
  description: typeof description === 'string' ? description.trim() : '',
197
268
  maxIterations: conf.maxIterations || 25,
198
269
  lang: detectedLang,
270
+ cwd: data.cwd,
199
271
  }, sid);
200
272
 
201
273
  const startPrompt = detectedLang === 'zh'
@@ -309,8 +381,11 @@ export function registerRoutes(ctx, {
309
381
  res.statusCode = 400;
310
382
  return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
311
383
  }
312
- const safeCwd = getSafeCwd(data.cwd);
313
- if (data.cwd && !safeCwd) {
384
+ const sessionGoal = engine.getGoal(sid);
385
+ const allowedRoots = [process.cwd()];
386
+ if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
387
+ const safeCwd = getSafeCwd(data.cwd, allowedRoots);
388
+ if (!safeCwd) {
314
389
  res.statusCode = 400;
315
390
  return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
316
391
  }
@@ -329,8 +404,11 @@ export function registerRoutes(ctx, {
329
404
  res.statusCode = 404;
330
405
  return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
331
406
  }
332
- const safeCwd = getSafeCwd(data.cwd);
333
- if (data.cwd && !safeCwd) {
407
+ const sessionGoal = engine.getGoal(sid);
408
+ const allowedRoots = [process.cwd()];
409
+ if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
410
+ const safeCwd = getSafeCwd(data.cwd, allowedRoots);
411
+ if (!safeCwd) {
334
412
  res.statusCode = 400;
335
413
  return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
336
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Autonomous Goal Execution & Multi-Turn Task Tracking Engine with Sticky Header for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",