@goodandready/dsh-cron 0.2.32 → 0.2.33
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-webhook.js +47 -4
- package/lib/api.js +3 -2
- package/lib/client.js +16 -2
- package/lib/runner-worktree.js +14 -10
- package/lib/runner.js +4 -7
- package/lib/runtimes.js +25 -6
- package/lib/settings.js +2 -1
- package/lib/store.js +1 -1
- package/lib/task-patch.js +22 -2
- package/lib/task-transfer.js +14 -3
- package/lib/telegram-commands.js +8 -0
- package/package.json +1 -1
package/lib/api-webhook.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import { logger } from './logger.js';
|
|
2
3
|
import { sendJson, readBody } from './http-utils.js';
|
|
3
4
|
import { answerTelegramCallbackQuery, sendTelegramMessage, getDshDefaultTelegramCredentials } from './telegram.js';
|
|
@@ -5,6 +6,7 @@ import { handleTelegramCommand } from './telegram-commands.js';
|
|
|
5
6
|
import { bestEffort } from './best-effort.js';
|
|
6
7
|
|
|
7
8
|
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
9
|
+
const seenUpdateIds = new Set();
|
|
8
10
|
|
|
9
11
|
export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
10
12
|
if (req.method !== 'POST') {
|
|
@@ -19,13 +21,45 @@ export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
|
19
21
|
const botToken = settings.botToken || defaults.botToken;
|
|
20
22
|
const allowedChatId = String(settings.chatId || defaults.chatId || '').trim();
|
|
21
23
|
|
|
24
|
+
// Secret token verification (Issue #208)
|
|
25
|
+
const webhookSecret = settings.telegramWebhookSecret || settings.webhookSecret || settings.secretToken || process.env.TELEGRAM_WEBHOOK_SECRET;
|
|
26
|
+
if (webhookSecret) {
|
|
27
|
+
const receivedSecret = req.headers['x-telegram-bot-api-secret-token'];
|
|
28
|
+
if (!receivedSecret || typeof receivedSecret !== 'string') {
|
|
29
|
+
logger.warn('[dsh-cron] unauthorized telegram webhook: missing secret token header');
|
|
30
|
+
sendJson(res, 401, { ok: false, error: 'Unauthorized: missing secret token' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const secretBuf = Buffer.from(String(webhookSecret));
|
|
34
|
+
const receivedBuf = Buffer.from(String(receivedSecret));
|
|
35
|
+
if (secretBuf.length !== receivedBuf.length || !crypto.timingSafeEqual(secretBuf, receivedBuf)) {
|
|
36
|
+
logger.warn('[dsh-cron] unauthorized telegram webhook: invalid secret token');
|
|
37
|
+
sendJson(res, 401, { ok: false, error: 'Unauthorized: invalid secret token' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deduplicate updates by update_id (Issue #208)
|
|
43
|
+
if (body.update_id != null) {
|
|
44
|
+
if (seenUpdateIds.has(body.update_id)) {
|
|
45
|
+
sendJson(res, 200, { ok: true, duplicate: true });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
seenUpdateIds.add(body.update_id);
|
|
49
|
+
if (seenUpdateIds.size > 2000) {
|
|
50
|
+
const first = seenUpdateIds.values().next().value;
|
|
51
|
+
seenUpdateIds.delete(first);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
22
55
|
// 1. Text command message (#175)
|
|
23
56
|
const message = body.message;
|
|
24
57
|
if (message && typeof message.text === 'string' && message.text.trim().startsWith('/')) {
|
|
25
58
|
const fromId = String(message.from?.id || '');
|
|
26
59
|
const chatId = String(message.chat?.id || fromId);
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
// Fail-closed: empty allowedChatId denies control (Issue #208)
|
|
61
|
+
if (!allowedChatId || (chatId !== allowedChatId && fromId !== allowedChatId)) {
|
|
62
|
+
logger.warn(`[dsh-cron] unauthorized telegram command from chat ${chatId} / user ${fromId} (allowedChatId: ${allowedChatId || 'NOT_CONFIGURED'})`);
|
|
29
63
|
sendJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
30
64
|
return;
|
|
31
65
|
}
|
|
@@ -52,8 +86,8 @@ export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
|
52
86
|
const fromId = String(callbackQuery.from?.id || '');
|
|
53
87
|
const chatId = String(callbackQuery.message?.chat?.id || fromId);
|
|
54
88
|
|
|
55
|
-
if (allowedChatId
|
|
56
|
-
logger.warn(`[dsh-cron] unauthorized telegram callback from chat ${chatId} / user ${fromId}`);
|
|
89
|
+
if (!allowedChatId || (chatId !== allowedChatId && fromId !== allowedChatId)) {
|
|
90
|
+
logger.warn(`[dsh-cron] unauthorized telegram callback from chat ${chatId} / user ${fromId} (allowedChatId: ${allowedChatId || 'NOT_CONFIGURED'})`);
|
|
57
91
|
if (botToken) {
|
|
58
92
|
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Unauthorized', showAlert: true });
|
|
59
93
|
}
|
|
@@ -82,6 +116,15 @@ export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
|
82
116
|
return;
|
|
83
117
|
}
|
|
84
118
|
|
|
119
|
+
const isConfigTask = task.source === 'config' || task.origin === 'config' || task.readOnly || task.managedBy === 'config';
|
|
120
|
+
if ((verb === 'pause' || verb === 'resume') && isConfigTask) {
|
|
121
|
+
if (botToken) {
|
|
122
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Cannot modify config-owned task', showAlert: true });
|
|
123
|
+
}
|
|
124
|
+
sendJson(res, 400, { ok: false, error: 'Cannot modify config-owned task' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
85
128
|
if (verb === 'run') {
|
|
86
129
|
try {
|
|
87
130
|
await scheduler.triggerManualRun(taskId);
|
package/lib/api.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildDuplicateTask,
|
|
18
18
|
RESERVED_TASK_IDS,
|
|
19
19
|
PATCHABLE_TASK_FIELDS,
|
|
20
|
+
isCodeExecutingTask,
|
|
20
21
|
} from './task-transfer.js';
|
|
21
22
|
import {
|
|
22
23
|
handleTaskExport,
|
|
@@ -190,8 +191,8 @@ async function createOrUpdateTask({ store, scheduler, req, res, apiToken }) {
|
|
|
190
191
|
return;
|
|
191
192
|
}
|
|
192
193
|
const taskType = normalizeTaskType(body.type);
|
|
193
|
-
if (CODE_EXECUTING_TYPES.includes(taskType) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
194
|
-
sendJson(res, 403, { ok: false, error: `Creating
|
|
194
|
+
if ((CODE_EXECUTING_TYPES.includes(taskType) || isCodeExecutingTask(body)) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
195
|
+
sendJson(res, 403, { ok: false, error: `Creating code-executing tasks over HTTP requires the x-dsh-cron-confirm: script header` });
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
const typeError = validateTaskType(taskType, body);
|
package/lib/client.js
CHANGED
|
@@ -1530,10 +1530,19 @@ window.__ModuleLoader__.load({
|
|
|
1530
1530
|
const RESERVED_TASK_IDS = ['export', 'import'];
|
|
1531
1531
|
|
|
1532
1532
|
/** Import needs the script-confirm header when the file has code tasks. */
|
|
1533
|
+
const isCodeTaskForm = (x) => {
|
|
1534
|
+
if (!x || typeof x !== 'object') return false;
|
|
1535
|
+
if (CODE_FORM_TYPES.includes(x.type)) return true;
|
|
1536
|
+
if (x.command && String(x.command).trim()) return true;
|
|
1537
|
+
if (x.preflightType === 'command' || (x.preflightCommand && String(x.preflightCommand).trim())) return true;
|
|
1538
|
+
if (x.selfHealingCommand && String(x.selfHealingCommand).trim()) return true;
|
|
1539
|
+
return false;
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1533
1542
|
const importHeaders = (document_) => {
|
|
1534
1543
|
const headers = { 'Content-Type': 'application/json' };
|
|
1535
1544
|
const tasks = (document_ && Array.isArray(document_.tasks)) ? document_.tasks : [];
|
|
1536
|
-
if (tasks.some(
|
|
1545
|
+
if (tasks.some(isCodeTaskForm)) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
1537
1546
|
return headers;
|
|
1538
1547
|
};
|
|
1539
1548
|
|
|
@@ -1976,7 +1985,12 @@ window.__ModuleLoader__.load({
|
|
|
1976
1985
|
// Cross-origin hardening: every code-executing type needs the explicit
|
|
1977
1986
|
// confirm header, which forged cross-site posts cannot attach (#86)
|
|
1978
1987
|
const headers = { 'Content-Type': 'application/json' };
|
|
1979
|
-
|
|
1988
|
+
const hasCodeExec = CODE_FORM_TYPES.includes(formType) ||
|
|
1989
|
+
Boolean(payload.command && payload.command.trim()) ||
|
|
1990
|
+
payload.preflightType === 'command' ||
|
|
1991
|
+
Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
|
|
1992
|
+
Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
|
|
1993
|
+
if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
1980
1994
|
const res = await fetch('/dsh-cron/tasks', {
|
|
1981
1995
|
method: 'POST',
|
|
1982
1996
|
headers,
|
package/lib/runner-worktree.js
CHANGED
|
@@ -7,16 +7,20 @@ import { bestEffort } from './best-effort.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export function resolveCwd(task, ctx = null) {
|
|
9
9
|
if (task.cwd) return String(task.cwd);
|
|
10
|
-
if (task.workspaceId
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
if (task.workspaceId) {
|
|
11
|
+
if (ctx && typeof ctx.get === 'function') {
|
|
12
|
+
const resolved = bestEffort('resolve-workspace-path', () => {
|
|
13
|
+
const workspaces = ctx.get('workspaces');
|
|
14
|
+
if (workspaces && typeof workspaces.get === 'function') {
|
|
15
|
+
const ws = workspaces.get(task.workspaceId);
|
|
16
|
+
return ws && ws.path ? String(ws.path) : null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
});
|
|
20
|
+
if (resolved) return resolved;
|
|
21
|
+
}
|
|
22
|
+
// Issue #231: fail-closed if workspaceId was specified but cannot be resolved
|
|
23
|
+
throw new Error(`Workspace "${task.workspaceId}" requested by task could not be resolved`);
|
|
20
24
|
}
|
|
21
25
|
return process.cwd();
|
|
22
26
|
}
|
package/lib/runner.js
CHANGED
|
@@ -297,18 +297,15 @@ export class SessionRunner {
|
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
/** Working directory for the run, in an isolated worktree when requested (#29). */
|
|
300
|
+
/** Working directory for the run, in an isolated worktree when requested (#29, #231). */
|
|
301
301
|
async _resolveRunContext(task) {
|
|
302
302
|
const baseCwd = this.resolveCwd(task);
|
|
303
303
|
let cwd = baseCwd;
|
|
304
304
|
let taskWorktree = null;
|
|
305
305
|
if (task.worktree) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
} catch (wtErr) {
|
|
310
|
-
logger.warn('[dsh-cron] task worktree not created, running in the workspace:', wtErr.message);
|
|
311
|
-
}
|
|
306
|
+
// Issue #231: Fail-closed — if worktree creation fails, abort run and preserve base workspace
|
|
307
|
+
taskWorktree = await this.createTaskWorktree(baseCwd, task);
|
|
308
|
+
cwd = taskWorktree;
|
|
312
309
|
}
|
|
313
310
|
return { baseCwd, cwd, taskWorktree };
|
|
314
311
|
}
|
package/lib/runtimes.js
CHANGED
|
@@ -123,18 +123,34 @@ export function buildSshInvocation(task) {
|
|
|
123
123
|
return { file: 'ssh', args };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
export const DOCKER_HOST_ENV_ALLOWLIST = ['TZ', 'LANG', 'LC_ALL'];
|
|
127
|
+
|
|
128
|
+
/** #8/#226: build Docker run invocation without leaking host process secrets into container. */
|
|
129
|
+
export function buildDockerInvocation(task, explicitEnv) {
|
|
128
130
|
const image = String(task.dockerImage || '').trim();
|
|
129
131
|
if (!image) throw new Error('Docker task requires dockerImage');
|
|
130
132
|
const args = ['run', '--rm'];
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
// Issue #226: isolate container environment. If explicitEnv is passed directly
|
|
135
|
+
// (e.g. test or explicit invocation), use it; otherwise use task.env only.
|
|
136
|
+
let envMap = explicitEnv;
|
|
137
|
+
if (!envMap) {
|
|
138
|
+
envMap = {};
|
|
139
|
+
if (task && task.env && typeof task.env === 'object') {
|
|
140
|
+
for (const [k, v] of Object.entries(task.env)) {
|
|
141
|
+
if (v !== null && v !== undefined) envMap[k] = String(v);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(envMap)) {
|
|
132
147
|
if (key === 'PATH' || key === 'HOME') continue; // container owns those
|
|
148
|
+
if (value === null || value === undefined) continue;
|
|
133
149
|
args.push('-e', `${key}=${value}`);
|
|
134
150
|
}
|
|
135
151
|
if (task.cwd) args.push('-v', `${task.cwd}:/workspace`, '-w', '/workspace');
|
|
136
152
|
args.push(image, 'sh', '-lc', String(task.prompt || 'true'));
|
|
137
|
-
return { file: 'docker', args };
|
|
153
|
+
return { file: 'docker', args, env: envMap };
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
/** #38: merge task env vars over the base environment. */
|
|
@@ -165,8 +181,11 @@ export function buildInvocation(task, cwd, baseEnv) {
|
|
|
165
181
|
return { type, env, http: buildHttpRequest(task) };
|
|
166
182
|
case 'ssh':
|
|
167
183
|
return { type, env, ...buildSshInvocation(task) };
|
|
168
|
-
case 'docker':
|
|
169
|
-
|
|
184
|
+
case 'docker': {
|
|
185
|
+
// Issue #226: do not pass full host process.env into docker run
|
|
186
|
+
const dockerInvocation = buildDockerInvocation(task, task.env);
|
|
187
|
+
return { type, env: dockerInvocation.env, file: dockerInvocation.file, args: dockerInvocation.args };
|
|
188
|
+
}
|
|
170
189
|
case 'script':
|
|
171
190
|
default:
|
|
172
191
|
return { type: 'script', env, file: null, args: null, shellCommand: String(task.prompt || '') };
|
package/lib/settings.js
CHANGED
|
@@ -15,6 +15,7 @@ if (typeof z.prototype?.volatile !== 'function') {
|
|
|
15
15
|
export const Config = z.object({
|
|
16
16
|
botToken: z.string().role('secret').default('').description('Legacy Telegram Bot API token (prefer botTokenRef with the DSH credentials service)').volatile(),
|
|
17
17
|
botTokenRef: z.string().default('').description('Name of the DSH credential that holds the Telegram bot token').volatile(),
|
|
18
|
+
telegramWebhookSecret: z.string().role('secret').default('').description('Secret token for validating incoming Telegram webhook requests (X-Telegram-Bot-Api-Secret-Token)').volatile(),
|
|
18
19
|
chatId: z.string().default('').description('Telegram chat ID that receives task reports').volatile(),
|
|
19
20
|
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task').volatile(),
|
|
20
21
|
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs').volatile(),
|
|
@@ -75,7 +76,7 @@ export function unwrapConfig(cfg) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export const SETTINGS_SYNC_KEYS = [
|
|
78
|
-
'botToken', 'botTokenRef', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
|
79
|
+
'botToken', 'botTokenRef', 'telegramWebhookSecret', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
|
79
80
|
'template', 'channelTemplates', 'deliveryTimeoutMs', 'silentRuleModel', 'inspectorModel',
|
|
80
81
|
'discordWebhookUrl', 'slackWebhookUrl',
|
|
81
82
|
'ntfyUrl', 'ntfyTopic', 'ntfyTokenRef',
|
package/lib/store.js
CHANGED
|
@@ -215,7 +215,7 @@ export class TaskStore {
|
|
|
215
215
|
* fields (botTokenRef, ntfyTokenRef, …) take precedence over them.
|
|
216
216
|
*/
|
|
217
217
|
static get MASKED_SETTING_KEYS() {
|
|
218
|
-
return ['botToken', 'discordWebhookUrl', 'slackWebhookUrl', 'barkKey', 'apiToken'];
|
|
218
|
+
return ['botToken', 'discordWebhookUrl', 'slackWebhookUrl', 'barkKey', 'apiToken', 'telegramWebhookSecret'];
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
saveSettings(newSettings = {}) {
|
package/lib/task-patch.js
CHANGED
|
@@ -22,6 +22,26 @@ 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). */
|
|
26
|
+
export function isCodeExecutionIntroduced(current, body) {
|
|
27
|
+
if (!current || !body) return false;
|
|
28
|
+
if (body.type !== undefined && isCodeTypeSwitch(current, body.type)) return true;
|
|
29
|
+
|
|
30
|
+
const hadPreflight = Boolean(current.preflightCommand && String(current.preflightCommand).trim()) || current.preflightType === 'command';
|
|
31
|
+
const hasPreflight = Boolean(body.preflightCommand && String(body.preflightCommand).trim()) || body.preflightType === 'command';
|
|
32
|
+
if (hasPreflight && !hadPreflight) return true;
|
|
33
|
+
|
|
34
|
+
const hadSelfHealing = Boolean(current.selfHealingCommand && String(current.selfHealingCommand).trim());
|
|
35
|
+
const hasSelfHealing = Boolean(body.selfHealingCommand && String(body.selfHealingCommand).trim());
|
|
36
|
+
if (hasSelfHealing && !hadSelfHealing) return true;
|
|
37
|
+
|
|
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
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
/**
|
|
26
46
|
* Normalise a patch: whitelist the fields, resolve the type, re-parse the
|
|
27
47
|
* schedule so cron patterns and one-shot markers stay consistent.
|
|
@@ -85,11 +105,11 @@ export function applyTaskPatch({ store, scheduler, id, body, allowCodeSwitch = f
|
|
|
85
105
|
if (isConfigOwned(current)) {
|
|
86
106
|
return { ok: false, configOwned: true, error: configOwnedMessage(id) };
|
|
87
107
|
}
|
|
88
|
-
if (
|
|
108
|
+
if (isCodeExecutionIntroduced(current, body) && allowCodeSwitch !== true) {
|
|
89
109
|
return {
|
|
90
110
|
ok: false,
|
|
91
111
|
needsConfirmation: true,
|
|
92
|
-
error: `
|
|
112
|
+
error: `Adding code-executing commands or switching this task to execute code requires explicit confirmation`,
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
115
|
const built = buildTaskPatch(current, body);
|
package/lib/task-transfer.js
CHANGED
|
@@ -222,10 +222,21 @@ export function validateImportDocument(doc) {
|
|
|
222
222
|
return { ok: true, tasks };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
/** True when
|
|
226
|
-
export function
|
|
225
|
+
/** True when a task executes host code or contains shell hooks (#212). */
|
|
226
|
+
export function isCodeExecutingTask(task) {
|
|
227
|
+
if (!task || typeof task !== 'object') return false;
|
|
227
228
|
const CODE_TYPES = ['script', 'node', 'python', 'ssh', 'docker'];
|
|
228
|
-
|
|
229
|
+
if (CODE_TYPES.includes(normalizeTaskType(task.type))) return true;
|
|
230
|
+
if (task.command && String(task.command).trim()) return true;
|
|
231
|
+
if (task.preflightType === 'command' || (task.preflightCommand && String(task.preflightCommand).trim())) return true;
|
|
232
|
+
if (task.selfHealingCommand && String(task.selfHealingCommand).trim()) return true;
|
|
233
|
+
if (task.preflightScript && String(task.preflightScript).trim()) return true;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** True when any task in a validated batch executes code on the host (#212). */
|
|
238
|
+
export function hasCodeExecutingTask(tasks) {
|
|
239
|
+
return (tasks || []).some(isCodeExecutingTask);
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/**
|
package/lib/telegram-commands.js
CHANGED
|
@@ -210,6 +210,10 @@ export async function handleTelegramCommand({
|
|
|
210
210
|
await reply(`❌ Task \`${targetId}\` not found.`);
|
|
211
211
|
return { ok: false, error: 'Task not found' };
|
|
212
212
|
}
|
|
213
|
+
if (task.source === 'config' || task.origin === 'config' || task.readOnly || task.managedBy === 'config') {
|
|
214
|
+
await reply(`❌ Cannot modify config-owned task *${task.title}* (\`${task.id}\`).`);
|
|
215
|
+
return { ok: false, error: 'Cannot modify config-owned task' };
|
|
216
|
+
}
|
|
213
217
|
scheduler.pauseTask(task.id, 'Paused via Telegram');
|
|
214
218
|
await reply(`⏸ Task *${task.title}* (\`${task.id}\`) paused.`);
|
|
215
219
|
return { ok: true, command, taskId: task.id };
|
|
@@ -226,6 +230,10 @@ export async function handleTelegramCommand({
|
|
|
226
230
|
await reply(`❌ Task \`${targetId}\` not found.`);
|
|
227
231
|
return { ok: false, error: 'Task not found' };
|
|
228
232
|
}
|
|
233
|
+
if (task.source === 'config' || task.origin === 'config' || task.readOnly || task.managedBy === 'config') {
|
|
234
|
+
await reply(`❌ Cannot modify config-owned task *${task.title}* (\`${task.id}\`).`);
|
|
235
|
+
return { ok: false, error: 'Cannot modify config-owned task' };
|
|
236
|
+
}
|
|
229
237
|
scheduler.resumeTask(task.id);
|
|
230
238
|
await reply(`▶️ Task *${task.title}* (\`${task.id}\`) resumed.`);
|
|
231
239
|
return { ok: true, command, taskId: task.id };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
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",
|