@goodandready/dsh-cron 0.2.47 → 0.2.48
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/lib/api.js +1 -1
- package/lib/channels.js +15 -2
- package/lib/config-jobs.js +2 -1
- package/lib/cron-tool.js +3 -2
- package/lib/http-utils.js +6 -4
- package/lib/index.js +11 -14
- package/lib/routes.js +1 -1
- package/lib/scheduler-execution.js +15 -14
- package/lib/scheduler.js +10 -8
- package/lib/settings.js +3 -1
- package/lib/task-create.js +2 -1
- package/lib/task-patch.js +11 -7
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -547,7 +547,7 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
if (req.method === 'POST') {
|
|
550
|
-
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action
|
|
550
|
+
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action });
|
|
551
551
|
if (handled) return;
|
|
552
552
|
}
|
|
553
553
|
if (req.method === 'GET' && !action) {
|
package/lib/channels.js
CHANGED
|
@@ -153,14 +153,27 @@ export function buildGiteaIssuePayload({ task, runInfo, settings = {} }) {
|
|
|
153
153
|
const failed = isFailed(runInfo.status);
|
|
154
154
|
const defaultNames = failed ? ['cron', 'bug', 'alert'] : ['cron', 'auto'];
|
|
155
155
|
|
|
156
|
-
// Resolve labels to integer IDs per Gitea Swagger (array<int64>) (#224)
|
|
157
|
-
|
|
156
|
+
// Resolve labels to integer IDs per Gitea Swagger (array<int64>) (#224, #297)
|
|
157
|
+
let mapping = settings.giteaLabelIds || {};
|
|
158
|
+
if (typeof mapping === 'string' && mapping.trim().startsWith('{')) {
|
|
159
|
+
try {
|
|
160
|
+
mapping = JSON.parse(mapping);
|
|
161
|
+
} catch (_) {
|
|
162
|
+
mapping = {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
158
165
|
let candidateIds = [];
|
|
159
166
|
|
|
160
167
|
if (Array.isArray(task && task.giteaLabelIds)) {
|
|
161
168
|
candidateIds = task.giteaLabelIds;
|
|
162
169
|
} else if (Array.isArray(settings.giteaLabels)) {
|
|
163
170
|
candidateIds = settings.giteaLabels;
|
|
171
|
+
} else if (typeof settings.giteaLabels === 'string' && settings.giteaLabels.trim()) {
|
|
172
|
+
candidateIds = settings.giteaLabels.split(',').map((s) => s.trim()).filter(Boolean).map((name) => {
|
|
173
|
+
const num = Number(name);
|
|
174
|
+
if (Number.isInteger(num) && num > 0) return num;
|
|
175
|
+
return typeof mapping === 'object' && mapping !== null ? mapping[name] : undefined;
|
|
176
|
+
});
|
|
164
177
|
} else if (Array.isArray(settings.giteaLabelIds)) {
|
|
165
178
|
candidateIds = settings.giteaLabelIds;
|
|
166
179
|
} else if (Array.isArray(task && task.labels)) {
|
package/lib/config-jobs.js
CHANGED
|
@@ -17,6 +17,7 @@ import { parseScheduleExpression } from './scheduler.js';
|
|
|
17
17
|
import { unknownChannelIds } from './channels.js';
|
|
18
18
|
import { PATCHABLE_TASK_FIELDS, validateTaskType } from './task-transfer.js';
|
|
19
19
|
import { pickPatchableFields } from './http-utils.js';
|
|
20
|
+
import { logger } from './logger.js';
|
|
20
21
|
|
|
21
22
|
/** Marker that makes a task owned by the config (#50). */
|
|
22
23
|
export const MANAGED_BY_CONFIG = 'config';
|
|
@@ -171,7 +172,7 @@ export function planConfigSync(entries, existingTasks) {
|
|
|
171
172
|
* Apply the plan: create, update and remove tasks, then arm or pause them.
|
|
172
173
|
* Never throws for a single bad entry — that entry is reported and skipped.
|
|
173
174
|
*/
|
|
174
|
-
export function applyConfigSync({ store, scheduler, entries, log =
|
|
175
|
+
export function applyConfigSync({ store, scheduler, entries, log = logger }) {
|
|
175
176
|
const plan = planConfigSync(entries, store.list({ status: 'all' }));
|
|
176
177
|
for (const warning of plan.warnings) log.warn('[dsh-cron] config jobs: ' + warning);
|
|
177
178
|
for (const entry of plan.skipped) log.error('[dsh-cron] config jobs: skipped: ' + entry.error);
|
package/lib/cron-tool.js
CHANGED
|
@@ -3,6 +3,7 @@ import { applyTaskPatch, describeTaskPatch } from './task-patch.js';
|
|
|
3
3
|
import { isConfigOwned, configOwnedMessage } from './config-jobs.js';
|
|
4
4
|
import { TASK_TYPES } from './runtimes.js';
|
|
5
5
|
import { CHANNEL_IDS } from './channels.js';
|
|
6
|
+
import { redactTaskSecrets } from './http-utils.js';
|
|
6
7
|
|
|
7
8
|
export const LEGACY_TOOL_NAMES = [
|
|
8
9
|
'cron_create_task',
|
|
@@ -394,7 +395,7 @@ export async function executeCronTool(store, scheduler, args = {}) {
|
|
|
394
395
|
`channels: ${Array.isArray(task.channels) && task.channels.length ? task.channels.join(', ') : '(legacy flags)'}`,
|
|
395
396
|
`prompt: ${String(task.prompt || '').slice(0, 500)}`,
|
|
396
397
|
].filter(Boolean).join('\n');
|
|
397
|
-
return { success: true, message: summary, task };
|
|
398
|
+
return { success: true, message: summary, task: redactTaskSecrets(task) };
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
case 'update': {
|
|
@@ -411,7 +412,7 @@ export async function executeCronTool(store, scheduler, args = {}) {
|
|
|
411
412
|
return {
|
|
412
413
|
success: true,
|
|
413
414
|
message: `Task "${result.task.title}" updated (${result.task.scheduleText || result.task.schedule}). Changed: ${describeTaskPatch(result.task, result.patch)}`,
|
|
414
|
-
task: result.task,
|
|
415
|
+
task: redactTaskSecrets(result.task),
|
|
415
416
|
};
|
|
416
417
|
}
|
|
417
418
|
|
package/lib/http-utils.js
CHANGED
|
@@ -94,15 +94,17 @@ export function isTrustedRequest(req, options = {}) {
|
|
|
94
94
|
|
|
95
95
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress;
|
|
96
96
|
const hasRemote = Boolean(remote && typeof remote === 'string');
|
|
97
|
-
const isLoopback =
|
|
97
|
+
const isLoopback = Boolean(hasRemote && isLoopbackAddress(remote));
|
|
98
98
|
|
|
99
99
|
const token = extractBearerToken(req);
|
|
100
100
|
const expectedToken = options.apiToken || process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN;
|
|
101
101
|
const isTokenAuthenticated = Boolean(expectedToken && token && token === expectedToken);
|
|
102
102
|
|
|
103
|
-
// 1. Non-loopback
|
|
104
|
-
if (
|
|
105
|
-
|
|
103
|
+
// 1. Non-loopback socket callers (or callers with missing remoteAddress) require explicit token authentication (#86, #295)
|
|
104
|
+
if (req.socket || req.connection || hasRemote) {
|
|
105
|
+
if (!isLoopback && !isTokenAuthenticated) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// 2. Validate Origin header if present: cannot be null, must match Host
|
package/lib/index.js
CHANGED
|
@@ -117,22 +117,20 @@ export {
|
|
|
117
117
|
LEGACY_ACTION_MAP,
|
|
118
118
|
};
|
|
119
119
|
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
120
|
-
// Helper to access configuration safely in native Cordis Context without throwing missing-inject error (#235)
|
|
121
|
-
function getContextConfig(ctx, rawConfig) {
|
|
120
|
+
// Helper to access configuration safely in native Cordis Context without throwing missing-inject error (#235, #288)
|
|
121
|
+
export function getContextConfig(ctx, rawConfig) {
|
|
122
122
|
try {
|
|
123
123
|
if (ctx && ctx.fiber && ctx.fiber.config) return ctx.fiber.config;
|
|
124
124
|
if (ctx && ctx.scope && ctx.scope.config) return ctx.scope.config;
|
|
125
125
|
if (ctx && ctx.runtime && ctx.runtime.config) return ctx.runtime.config;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Ignore missing inject error on native Cordis Context (#235)
|
|
132
|
-
}
|
|
126
|
+
// In plain/mock contexts without fiber/scope, safely attempt reading config
|
|
127
|
+
try {
|
|
128
|
+
if (ctx && ctx.config) return ctx.config;
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// Ignored if ctx is a Cordis proxy throwing without inject (#235, #288)
|
|
133
131
|
}
|
|
134
132
|
} catch (err) {
|
|
135
|
-
// Ignore context access error and fall back to rawConfig (#235)
|
|
133
|
+
// Ignore context access error and fall back to rawConfig (#235, #288)
|
|
136
134
|
}
|
|
137
135
|
return rawConfig || {};
|
|
138
136
|
}
|
|
@@ -199,7 +197,7 @@ export function apply(ctx, rawConfig) {
|
|
|
199
197
|
// A broken entry is reported and skipped, never fatal.
|
|
200
198
|
try {
|
|
201
199
|
const configJobs = Array.isArray(config && config.jobs) ? config.jobs : [];
|
|
202
|
-
const summary = applyConfigSync({ store, scheduler, entries: configJobs });
|
|
200
|
+
const summary = applyConfigSync({ store, scheduler, entries: configJobs, log: logger });
|
|
203
201
|
logger.info('[dsh-cron] config jobs synced: ' + JSON.stringify(summary));
|
|
204
202
|
} catch (err) {
|
|
205
203
|
logger.error('[dsh-cron] config job sync failed:', err.message);
|
|
@@ -253,7 +251,7 @@ export function apply(ctx, rawConfig) {
|
|
|
253
251
|
}
|
|
254
252
|
const current = store.getSettings();
|
|
255
253
|
const patch = {};
|
|
256
|
-
for (const k of
|
|
254
|
+
for (const k of SETTINGS_SYNC_KEYS) {
|
|
257
255
|
if (unwrapped[k] !== undefined && unwrapped[k] !== current[k]) {
|
|
258
256
|
patch[k] = unwrapped[k];
|
|
259
257
|
}
|
|
@@ -295,7 +293,6 @@ export function apply(ctx, rawConfig) {
|
|
|
295
293
|
registerRoutes(ctx, {
|
|
296
294
|
store,
|
|
297
295
|
scheduler,
|
|
298
|
-
getCronSettingsScope: () => settingsService,
|
|
299
296
|
recommendations,
|
|
300
297
|
});
|
|
301
298
|
|
|
@@ -311,7 +308,7 @@ export function apply(ctx, rawConfig) {
|
|
|
311
308
|
...activeVolatileConfig,
|
|
312
309
|
};
|
|
313
310
|
const clientSettings = store.getClientSettings();
|
|
314
|
-
for (const k of
|
|
311
|
+
for (const k of SETTINGS_SYNC_KEYS) {
|
|
315
312
|
if (unwrapped[k] !== undefined) {
|
|
316
313
|
clientSettings[k] = unwrapped[k];
|
|
317
314
|
}
|
package/lib/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ import { sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
|
|
|
10
10
|
import { createKanbanCard } from './integrations.js';
|
|
11
11
|
import { sanitizeSettingsPayload, applySettingsToScope } from './settings.js';
|
|
12
12
|
|
|
13
|
-
export function registerRoutes(ctx, { store, scheduler,
|
|
13
|
+
export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
14
14
|
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
15
15
|
ctx.effect(() => ctx.webServer.register({
|
|
16
16
|
kind: 'prefix',
|
|
@@ -40,15 +40,17 @@ export async function executePreflight(task) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (type === 'http') {
|
|
43
|
+
let timeout;
|
|
43
44
|
try {
|
|
44
45
|
const controller = new AbortController();
|
|
45
|
-
|
|
46
|
+
timeout = setTimeout(() => controller.abort(), 5000);
|
|
46
47
|
const res = await fetch(target, { signal: controller.signal });
|
|
47
|
-
clearTimeout(timeout);
|
|
48
48
|
if (res.ok) return { ok: true };
|
|
49
49
|
return { ok: false, reason: `HTTP check returned status ${res.status}` };
|
|
50
50
|
} catch (err) {
|
|
51
51
|
return { ok: false, reason: `HTTP check failed: ${err.message}` };
|
|
52
|
+
} finally {
|
|
53
|
+
if (timeout) clearTimeout(timeout);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -595,7 +597,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
595
597
|
const guard = await checkAndApplyBurnGuard(scheduler, taskId, outcome);
|
|
596
598
|
if (guard && guard.exceeded) return;
|
|
597
599
|
|
|
598
|
-
|
|
600
|
+
if (!scheduler.isStopped && scheduler.queue.length > 0 && (scheduler.maxConcurrent === 0 || scheduler.running.size < scheduler.maxConcurrent)) {
|
|
599
601
|
let admittedIndex = -1;
|
|
600
602
|
for (let i = 0; i < scheduler.queue.length; i++) {
|
|
601
603
|
const candidate = scheduler.queue[i];
|
|
@@ -619,17 +621,16 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
619
621
|
break;
|
|
620
622
|
}
|
|
621
623
|
|
|
622
|
-
if (admittedIndex
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
break;
|
|
624
|
+
if (admittedIndex !== -1) {
|
|
625
|
+
const nextItem = scheduler.queue.splice(admittedIndex, 1)[0];
|
|
626
|
+
setImmediate(() => {
|
|
627
|
+
if (!scheduler.isStopped) {
|
|
628
|
+
scheduler.runTask(nextItem.taskId, nextItem.options || {}).catch((runErr) => {
|
|
629
|
+
bestEffort('drain-queued-task', () => {}, scheduler.logger);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
// 1. Structured LLM Actions (#137)
|
package/lib/scheduler.js
CHANGED
|
@@ -140,15 +140,17 @@ function parseIntervalExpression(str) {
|
|
|
140
140
|
if (num >= 60) {
|
|
141
141
|
if (num % 60 === 0) {
|
|
142
142
|
const hours = num / 60;
|
|
143
|
-
if (hours >= 24
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
if (hours >= 24) {
|
|
144
|
+
if (hours % 24 === 0) {
|
|
145
|
+
const days = hours / 24;
|
|
146
|
+
if (days > 31) {
|
|
147
|
+
throw new Error(`Invalid interval: minute interval "${num}m" (${days} days) exceeds 31 days`);
|
|
148
|
+
}
|
|
149
|
+
pattern = `0 0 */${days} * *`;
|
|
150
|
+
humanText = `Every ${days} days`;
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error(`Invalid interval: minute interval "${num}m" (${hours} hours) cannot be expressed as a cron step`);
|
|
147
153
|
}
|
|
148
|
-
pattern = `0 0 */${days} * *`;
|
|
149
|
-
humanText = `Every ${days} days`;
|
|
150
|
-
} else if (hours > 23) {
|
|
151
|
-
throw new Error(`Invalid interval: minute interval "${num}m" (${hours} hours) cannot be expressed as a cron step`);
|
|
152
154
|
} else {
|
|
153
155
|
pattern = `0 */${hours} * * *`;
|
|
154
156
|
humanText = `Every ${hours} hours`;
|
package/lib/settings.js
CHANGED
|
@@ -44,6 +44,8 @@ export const Config = z.object({
|
|
|
44
44
|
giteaBaseUrl: z.string().default('').description('Gitea base URL for alert issues').volatile(),
|
|
45
45
|
giteaRepo: z.string().default('').description('Gitea repository (owner/name) for alert issues').volatile(),
|
|
46
46
|
giteaTokenRef: z.string().default('').description('Credential name for the Gitea API token').volatile(),
|
|
47
|
+
giteaLabels: z.any().default([]).description('Gitea label names or IDs for alert issues').volatile(),
|
|
48
|
+
giteaLabelIds: z.any().default({}).description('Mapping of label name to integer ID for Gitea issues').volatile(),
|
|
47
49
|
prReviewerEnabled: z.boolean().default(false).description('Enable automated PR Reviewer scheduled agent (#33)').volatile(),
|
|
48
50
|
// --- Declarative jobs (#50, ADR-0001) ---
|
|
49
51
|
jobs: z.array(z.any()).default([]).description('Static jobs owned by the config; each entry needs id, title, schedule and (for agent types) prompt'),
|
|
@@ -79,7 +81,7 @@ export const SETTINGS_SYNC_KEYS = [
|
|
|
79
81
|
'pushplusTokenRef',
|
|
80
82
|
'pushplusUrl',
|
|
81
83
|
'ttsBaseUrl',
|
|
82
|
-
'giteaBaseUrl', 'giteaRepo', 'giteaTokenRef', 'prReviewerEnabled',
|
|
84
|
+
'giteaBaseUrl', 'giteaRepo', 'giteaTokenRef', 'giteaLabels', 'giteaLabelIds', 'prReviewerEnabled',
|
|
83
85
|
'apiToken',
|
|
84
86
|
];
|
|
85
87
|
|
package/lib/task-create.js
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizeTaskType } from './runtimes.js';
|
|
|
2
2
|
import { validateTaskType } from './task-transfer.js';
|
|
3
3
|
import { parseScheduleExpression } from './scheduler.js';
|
|
4
4
|
import { CHANNEL_IDS } from './channels.js';
|
|
5
|
+
import { redactTaskSecrets } from './http-utils.js';
|
|
5
6
|
|
|
6
7
|
export function executeCreateTask(store, scheduler, args) {
|
|
7
8
|
const taskType = normalizeTaskType(args.type);
|
|
@@ -76,7 +77,7 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
76
77
|
return {
|
|
77
78
|
success: true,
|
|
78
79
|
message: `Task "${task.title}" scheduled successfully (${task.scheduleText})`,
|
|
79
|
-
task
|
|
80
|
+
task: redactTaskSecrets(task),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
|
package/lib/task-patch.js
CHANGED
|
@@ -22,23 +22,27 @@ export function isCodeTypeSwitch(current, nextType) {
|
|
|
22
22
|
return CODE_EXECUTING_TYPES.includes(target) && !CODE_EXECUTING_TYPES.includes(current.type);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** True when a patch introduces code-executing type or shell hooks (#212). */
|
|
25
|
+
/** True when a patch introduces code-executing type or shell hooks (#212, #296). */
|
|
26
26
|
export function isCodeExecutionIntroduced(current, body) {
|
|
27
27
|
if (!current || !body) return false;
|
|
28
28
|
if (body.type !== undefined && isCodeTypeSwitch(current, body.type)) return true;
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const isShellPreflight = (t) => t === 'command' || t === 'shell';
|
|
31
|
+
const currentPreflightType = current.preflightType || 'none';
|
|
32
|
+
const nextPreflightType = body.preflightType !== undefined ? body.preflightType : currentPreflightType;
|
|
33
|
+
|
|
34
|
+
const currentPreflightTarget = current.preflightTarget || current.preflightCommand || '';
|
|
35
|
+
const bodyPreflightTarget = body.preflightTarget !== undefined ? body.preflightTarget : (body.preflightCommand !== undefined ? body.preflightCommand : '');
|
|
36
|
+
const nextPreflightTarget = (body.preflightTarget !== undefined || body.preflightCommand !== undefined) ? bodyPreflightTarget : currentPreflightTarget;
|
|
37
|
+
|
|
38
|
+
const hadPreflight = isShellPreflight(currentPreflightType) && Boolean(String(currentPreflightTarget).trim());
|
|
39
|
+
const hasPreflight = isShellPreflight(nextPreflightType) && Boolean(String(nextPreflightTarget).trim());
|
|
32
40
|
if (hasPreflight && !hadPreflight) return true;
|
|
33
41
|
|
|
34
42
|
const hadSelfHealing = Boolean(current.selfHealingCommand && String(current.selfHealingCommand).trim());
|
|
35
43
|
const hasSelfHealing = Boolean(body.selfHealingCommand && String(body.selfHealingCommand).trim());
|
|
36
44
|
if (hasSelfHealing && !hadSelfHealing) return true;
|
|
37
45
|
|
|
38
|
-
const hadCommand = Boolean(current.command && String(current.command).trim());
|
|
39
|
-
const hasCommand = Boolean(body.command && String(body.command).trim());
|
|
40
|
-
if (hasCommand && !hadCommand && !CODE_EXECUTING_TYPES.includes(current.type)) return true;
|
|
41
|
-
|
|
42
46
|
return false;
|
|
43
47
|
}
|
|
44
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|