clementine-agent 1.0.93 → 1.0.94

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.
@@ -5,6 +5,23 @@
5
5
  * cron job execution parameters: turn limits, models, timeouts, prompt
6
6
  * enrichment, escalation, and circuit-breaking.
7
7
  */
8
+ import { CronRunLog } from '../gateway/heartbeat.js';
8
9
  import type { CronJobDefinition, ExecutionAdvice } from '../types.js';
10
+ interface ReflectionEntry {
11
+ jobName: string;
12
+ timestamp: string;
13
+ existence: boolean;
14
+ substance: boolean;
15
+ actionable: boolean;
16
+ communication: boolean;
17
+ criteriaMet: boolean | null;
18
+ quality: number;
19
+ gap: string;
20
+ commNote: string;
21
+ }
9
22
  export declare function getExecutionAdvice(jobName: string, job: CronJobDefinition): ExecutionAdvice;
23
+ export declare function checkTurnLimitHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
24
+ export declare function checkReflectionQuality(reflections: ReflectionEntry[], job: CronJobDefinition, advice: ExecutionAdvice): void;
25
+ export declare function checkTimeoutHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
26
+ export {};
10
27
  //# sourceMappingURL=execution-advisor.d.ts.map
@@ -102,7 +102,11 @@ export function getExecutionAdvice(jobName, job) {
102
102
  return advice;
103
103
  }
104
104
  // ── Rule helpers ────────────────────────────────────────────────────
105
- function checkTurnLimitHits(runs, job, advice) {
105
+ export function checkTurnLimitHits(runs, job, advice) {
106
+ // Unleashed jobs manage per-phase turns via UNLEASHED_PHASE_TURNS, not job.maxTurns.
107
+ // Bumping maxTurns here would override the unleashed limit with a small value.
108
+ if (job.mode === 'unleashed')
109
+ return;
106
110
  // Use precise TerminalReason when available, fall back to regex on error text
107
111
  const turnLimitHits = runs.slice(0, 5).filter(r => {
108
112
  if (r.status !== 'error' && r.status !== 'retried')
@@ -131,7 +135,9 @@ function checkTurnLimitHits(runs, job, advice) {
131
135
  logger.debug({ job: job.name, from: currentMax, to: advice.adjustedMaxTurns }, 'Adjusting maxTurns due to turn-limit hits');
132
136
  }
133
137
  }
134
- function checkReflectionQuality(reflections, job, advice) {
138
+ export function checkReflectionQuality(reflections, job, advice) {
139
+ if (job.mode === 'unleashed')
140
+ return;
135
141
  const recent = reflections.slice(0, 5); // already newest-first
136
142
  if (recent.length < 3)
137
143
  return;
@@ -166,7 +172,9 @@ function checkModelUpgrade(runs, job, advice) {
166
172
  logger.debug({ job: job.name, failures: recentFailures.length }, 'Upgrading model from haiku to sonnet due to repeated failures');
167
173
  }
168
174
  }
169
- function checkTimeoutHits(runs, job, advice) {
175
+ export function checkTimeoutHits(runs, job, advice) {
176
+ if (job.mode === 'unleashed')
177
+ return;
170
178
  const timeoutMs = DEFAULT_TIMEOUT_MS; // standard cron timeout
171
179
  const threshold = timeoutMs * 0.95;
172
180
  const timeoutHits = runs.slice(0, 5).filter(r => {
@@ -14,11 +14,15 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
14
  import { randomBytes } from 'node:crypto';
15
15
  import path from 'node:path';
16
16
  import pino from 'pino';
17
- import { BASE_DIR } from '../config.js';
17
+ import { ALLOW_SOURCE_EDITS, BASE_DIR } from '../config.js';
18
18
  import { preflightSourceChange } from './source-preflight.js';
19
19
  import { recordSourceMod } from './source-mods.js';
20
20
  const logger = pino({ name: 'clementine.safe-restart' });
21
21
  const SENTINEL_PATH = path.join(BASE_DIR, '.restart-sentinel.json');
22
+ const DEPRECATION_MESSAGE = 'Source self-editing is disabled. Use advisor-rule YAML, CRON.md frontmatter, ' +
23
+ 'or prompt-override markdown instead — those land without a restart and survive ' +
24
+ 'npm updates. Set CLEMENTINE_ALLOW_SOURCE_EDITS=1 in ~/.clementine/.env only for ' +
25
+ 'genuine engine bugs that cannot be expressed as data.';
22
26
  /** Files that cannot be self-edited (security-critical or self-referential). */
23
27
  const BLOCKLIST = new Set([
24
28
  'src/config.ts',
@@ -47,6 +51,11 @@ const BLOCKLIST = new Set([
47
51
  */
48
52
  export async function safeSourceEdit(pkgDir, changes, opts) {
49
53
  const reason = opts?.reason ?? 'source self-edit';
54
+ // Quarantine: source self-edit is deprecated. Off by default.
55
+ if (!ALLOW_SOURCE_EDITS) {
56
+ logger.warn({ fileCount: changes.length, files: changes.map(c => c.relativePath), reason }, 'Source self-edit refused — primitive is disabled');
57
+ return { success: false, error: DEPRECATION_MESSAGE };
58
+ }
50
59
  // Validate against blocklist
51
60
  for (const change of changes) {
52
61
  if (BLOCKLIST.has(change.relativePath)) {
@@ -23,9 +23,11 @@ const DEFAULT_CONFIG = {
23
23
  maxDurationMs: 3_600_000, // 1 hour
24
24
  acceptThreshold: 0.7,
25
25
  plateauLimit: 3,
26
- areas: ['soul', 'cron', 'workflow', 'memory', 'agent', 'source', 'communication', 'goal'],
26
+ // 'source' deprecated self-improvement should produce data (cron, workflow, etc.),
27
+ // not engine TS edits. Re-add only with CLEMENTINE_ALLOW_SOURCE_EDITS=1.
28
+ areas: ['soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal'],
27
29
  autoApply: true,
28
- sourceMode: 'propose-only',
30
+ sourceMode: 'skip',
29
31
  };
30
32
  // ── Paths ────────────────────────────────────────────────────────────
31
33
  const EXPERIMENT_LOG = path.join(SELF_IMPROVE_DIR, 'experiment-log.jsonl');
@@ -6,10 +6,13 @@
6
6
  */
7
7
  import express from 'express';
8
8
  import pino from 'pino';
9
- import { WEBHOOK_PORT, WEBHOOK_SECRET } from '../config.js';
9
+ import { WEBHOOK_BIND, WEBHOOK_PORT, WEBHOOK_SECRET } from '../config.js';
10
10
  const logger = pino({ name: 'clementine.webhook' });
11
11
  // ── Entry point ───────────────────────────────────────────────────────
12
12
  export async function startWebhook(gateway) {
13
+ if (!WEBHOOK_SECRET) {
14
+ throw new Error('WEBHOOK_ENABLED=true requires WEBHOOK_SECRET to be set. Refusing to start an unauthenticated webhook server.');
15
+ }
13
16
  const app = express();
14
17
  app.use(express.json());
15
18
  // ── Bearer token auth middleware ──────────────────────────────────
@@ -54,8 +57,8 @@ export async function startWebhook(gateway) {
54
57
  res.status(500).json({ error: 'Internal server error' });
55
58
  }
56
59
  });
57
- // ── GET /api/status — health check ────────────────────────────────
58
- app.get('/api/status', (_req, res) => {
60
+ // ── GET /api/status — health check (auth-gated to avoid uptime leakage) ──
61
+ app.get('/api/status', requireAuth, (_req, res) => {
59
62
  res.json({
60
63
  status: 'ok',
61
64
  uptime: process.uptime(),
@@ -64,9 +67,13 @@ export async function startWebhook(gateway) {
64
67
  });
65
68
  // ── Start server ──────────────────────────────────────────────────
66
69
  const port = WEBHOOK_PORT;
70
+ const bind = WEBHOOK_BIND;
71
+ if (bind !== '127.0.0.1' && bind !== 'localhost') {
72
+ logger.warn({ bind }, '⚠ Webhook bound to non-localhost address — bearer auth is your only protection. Prefer tunneling (cloudflared) over exposing directly.');
73
+ }
67
74
  await new Promise((resolve) => {
68
- app.listen(port, '0.0.0.0', () => {
69
- logger.info(`Webhook API server listening on port ${port}`);
75
+ app.listen(port, bind, () => {
76
+ logger.info({ bind, port }, 'Webhook API server listening');
70
77
  resolve();
71
78
  });
72
79
  });
package/dist/config.d.ts CHANGED
@@ -80,6 +80,7 @@ export declare const WHATSAPP_WEBHOOK_PORT: number;
80
80
  export declare const WEBHOOK_ENABLED: boolean;
81
81
  export declare const WEBHOOK_PORT: number;
82
82
  export declare const WEBHOOK_SECRET: string;
83
+ export declare const WEBHOOK_BIND: string;
83
84
  export declare const GROQ_API_KEY: string;
84
85
  export declare const ELEVENLABS_API_KEY: string;
85
86
  export declare const ELEVENLABS_VOICE_ID: string;
@@ -150,6 +151,7 @@ export declare const PLANS_DIR: string;
150
151
  export declare const ADVISOR_LOG_PATH: string;
151
152
  export declare const REMOTE_ACCESS_CONFIG: string;
152
153
  export declare const STAGING_DIR: string;
154
+ export declare const ALLOW_SOURCE_EDITS: boolean;
153
155
  export declare const CLAUDE_CODE_OAUTH_TOKEN: string;
154
156
  export declare const ANTHROPIC_API_KEY: string;
155
157
  export declare const CREDENTIALS_FILE: string;
package/dist/config.js CHANGED
@@ -172,6 +172,9 @@ export const WHATSAPP_WEBHOOK_PORT = parseInt(getEnv('WHATSAPP_WEBHOOK_PORT', '8
172
172
  export const WEBHOOK_ENABLED = getEnv('WEBHOOK_ENABLED', 'false').toLowerCase() === 'true';
173
173
  export const WEBHOOK_PORT = parseInt(getEnv('WEBHOOK_PORT', '8420'), 10);
174
174
  export const WEBHOOK_SECRET = getSecret('WEBHOOK_SECRET');
175
+ // Default bind to localhost only — flip to 0.0.0.0 explicitly via WEBHOOK_BIND
176
+ // for tunneled or LAN-exposed setups. Avoids the OpenClaw CVE-2026-25253 shape.
177
+ export const WEBHOOK_BIND = getEnv('WEBHOOK_BIND', '127.0.0.1');
175
178
  // ── Voice ────────────────────────────────────────────────────────────
176
179
  export const GROQ_API_KEY = getSecret('GROQ_API_KEY');
177
180
  export const ELEVENLABS_API_KEY = getSecret('ELEVENLABS_API_KEY');
@@ -302,6 +305,15 @@ export const ADVISOR_LOG_PATH = path.join(BASE_DIR, 'cron', 'advisor-decisions.j
302
305
  export const REMOTE_ACCESS_CONFIG = path.join(BASE_DIR, 'remote-access.json');
303
306
  // ── Source Self-Edit Staging ─────────────────────────────────────────
304
307
  export const STAGING_DIR = path.join(BASE_DIR, 'staging');
308
+ // Source self-editing is deprecated. The data-driven path (advisor rules,
309
+ // CRON.md frontmatter, prompt overrides) is the supported way to evolve
310
+ // behavior without requiring a new release. Set CLEMENTINE_ALLOW_SOURCE_EDITS=1
311
+ // in ~/.clementine/.env to re-enable for genuine engine bugs that can't be
312
+ // expressed as data — the primitive itself stays on disk for that escape hatch.
313
+ export const ALLOW_SOURCE_EDITS = (() => {
314
+ const raw = getEnv('CLEMENTINE_ALLOW_SOURCE_EDITS', '').toLowerCase().trim();
315
+ return raw === '1' || raw === 'true' || raw === 'yes';
316
+ })();
305
317
  // ── API ──────────────────────────────────────────────────────────────
306
318
  // Long-lived OAuth token from `clementine login` / `claude setup-token`.
307
319
  // Takes priority over ANTHROPIC_API_KEY in the SDK subprocess env.
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import Anthropic from '@anthropic-ai/sdk';
16
16
  import { z } from 'zod';
17
17
  import { BASE_DIR, CRON_FILE, SYSTEM_DIR, env, getStore, logger, textResult, } from './shared.js';
18
+ import { ALLOW_SOURCE_EDITS } from '../config.js';
18
19
  import { getInteractionSource } from '../agent/hooks.js';
19
20
  import { renameSync } from 'node:fs';
20
21
  import * as keychain from '../secrets/keychain.js';
@@ -1628,11 +1629,22 @@ export function registerAdminTools(server) {
1628
1629
  // ── Source Self-Edit Tools ──────────────────────────────────────────────
1629
1630
  const SELF_IMPROVE_DIR = path.join(BASE_DIR, 'self-improve');
1630
1631
  const PENDING_SOURCE_DIR = path.join(SELF_IMPROVE_DIR, 'pending-source-changes');
1631
- server.tool('self_edit_source', 'Edit most Clementine TypeScript source files (for new features or bug fixes). Validates in a staging worktree, compiles, and triggers restart on success. Blocked files: `src/config.ts`, `src/gateway/security-scanner.ts`, `src/security/scanner.ts`. Do NOT use this tool to change user-tunable settings (budget caps, model tier, heartbeat interval, timezone, channel IDs, etc.) those live in `~/.clementine/.env` and are managed by the user via `clementine config set KEY value`, which survives `clementine update` / `npm update -g`.', {
1632
- file: z.string().describe('Path relative to src/ (e.g., "channels/discord-agent-bot.ts")'),
1632
+ server.tool('self_edit_source', 'DEPRECATED source self-editing is disabled. Improvements should be expressed as data, not TypeScript: edit advisor rules in ~/.clementine/advisor-rules/, CRON.md frontmatter, or prompt overrides. Those land without a restart and survive npm updates. Only re-enabled (CLEMENTINE_ALLOW_SOURCE_EDITS=1) for genuine engine bugs that cannot be expressed as data.', {
1633
+ file: z.string().describe('Path relative to src/'),
1633
1634
  content: z.string().describe('Complete new file content'),
1634
1635
  reason: z.string().describe('Why this change is being made'),
1635
- }, async ({ file, content, reason }) => {
1636
+ }, async ({ file, content: _content, reason }) => {
1637
+ if (!ALLOW_SOURCE_EDITS) {
1638
+ logger.warn({ file, reason }, 'self_edit_source called while disabled — rejecting');
1639
+ return textResult('Source self-editing is disabled. The fix you want belongs in user-space data, not engine TypeScript:\n' +
1640
+ ' • Cron job behavior → edit ~/.clementine/vault/00-System/CRON.md frontmatter\n' +
1641
+ ' • Advisor logic → write a YAML file in ~/.clementine/advisor-rules/ (coming in next phase)\n' +
1642
+ ' • Prompt content → edit the per-job prompt in CRON.md\n' +
1643
+ ' • Agent behavior → edit the agent.md / SOUL.md\n\n' +
1644
+ 'Those changes land without a restart and survive `npm update -g clementine`. ' +
1645
+ 'If this is a genuine engine bug that cannot be expressed as data, ask the owner to set ' +
1646
+ 'CLEMENTINE_ALLOW_SOURCE_EDITS=1 in ~/.clementine/.env.');
1647
+ }
1636
1648
  // Security blocklist
1637
1649
  const BLOCKLIST = ['config.ts', 'gateway/security-scanner.ts', 'security/scanner.ts'];
1638
1650
  if (BLOCKLIST.some(b => file === b || file.startsWith(b))) {
@@ -1646,7 +1658,7 @@ export function registerAdminTools(server) {
1646
1658
  const pending = {
1647
1659
  id,
1648
1660
  file: `src/${file}`,
1649
- content,
1661
+ content: _content,
1650
1662
  reason,
1651
1663
  createdAt: new Date().toISOString(),
1652
1664
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.93",
3
+ "version": "1.0.94",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",