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.
- package/dist/agent/execution-advisor.d.ts +17 -0
- package/dist/agent/execution-advisor.js +11 -3
- package/dist/agent/safe-restart.js +10 -1
- package/dist/agent/self-improve.js +4 -2
- package/dist/channels/webhook.js +12 -5
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -0
- package/dist/tools/admin-tools.js +16 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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: '
|
|
30
|
+
sourceMode: 'skip',
|
|
29
31
|
};
|
|
30
32
|
// ── Paths ────────────────────────────────────────────────────────────
|
|
31
33
|
const EXPERIMENT_LOG = path.join(SELF_IMPROVE_DIR, 'experiment-log.jsonl');
|
package/dist/channels/webhook.js
CHANGED
|
@@ -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,
|
|
69
|
-
logger.info(
|
|
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', '
|
|
1632
|
-
file: z.string().describe('Path relative to src/
|
|
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
|
};
|