shraga 0.1.157 → 0.1.159
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/defaults/skills/platform.md +1 -0
- package/defaults/skills/scheduler.md +2 -0
- package/dist/client/assets/{index-Vf-2jafh.js → index-CcLqqS5x.js} +173 -173
- package/dist/client/assets/{index-BLAQpfJ9.css → index-NtN1I_dh.css} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/client/components/ConfigPanel.tsx +29 -13
- package/src/client/components/schedules/ScheduleEditor.tsx +12 -2
- package/src/server/boot.ts +2 -1
- package/src/server/claude.ts +11 -1
- package/src/server/disk-search-guard.ts +47 -0
- package/src/server/engine/claude-code.ts +10 -2
- package/src/server/hooks.ts +21 -0
- package/src/server/scheduler/runner.ts +6 -1
- package/src/server/shraga-config.ts +2 -0
- package/src/server/skills.ts +3 -1
package/src/server/hooks.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { rewriteSlackMentions } from './slack/mention-rewrite.ts';
|
|
|
10
10
|
import { PROTECTED_DATA_MESSAGE, writesProtectedData } from './security/enforce.ts';
|
|
11
11
|
import { getOffload, type OffloadSettings } from './shraga-config.ts';
|
|
12
12
|
import { heavyCommandDenyMessage, heavyLocalCommand } from './offload.ts';
|
|
13
|
+
import { wholeDiskSearch, wholeDiskSearchMessage } from './disk-search-guard.ts';
|
|
14
|
+
import { DATA_DIR } from './paths.ts';
|
|
13
15
|
|
|
14
16
|
/** Patterns that indicate a long-running script the model should background. */
|
|
15
17
|
const LONG_RUNNING_PATTERNS = [
|
|
@@ -198,6 +200,24 @@ export const denyHeavyLocalMedia = (cfg: OffloadSettings): HookCallback => async
|
|
|
198
200
|
};
|
|
199
201
|
};
|
|
200
202
|
|
|
203
|
+
/** Refuse searches rooted at / or the bare home dir (Bash find/grep -r/mdfind, or the Grep/Glob tools' `path`). */
|
|
204
|
+
export const denyWholeDiskSearch: HookCallback = async (input) => {
|
|
205
|
+
if (input.hook_event_name !== 'PreToolUse') return {};
|
|
206
|
+
const { tool_name, tool_input } = input as PreToolUseHookInput;
|
|
207
|
+
const ti = (tool_input ?? {}) as Record<string, unknown>;
|
|
208
|
+
const what = tool_name === 'Bash' ? wholeDiskSearch(String(ti.command ?? ''))
|
|
209
|
+
: typeof ti.path === 'string' && wholeDiskSearch(`find ${ti.path}`) ? `${tool_name} ${ti.path}` : null;
|
|
210
|
+
if (!what) return {};
|
|
211
|
+
console.log(`[hooks] Denied whole-disk search: ${what}`);
|
|
212
|
+
return {
|
|
213
|
+
hookSpecificOutput: {
|
|
214
|
+
hookEventName: 'PreToolUse' as const,
|
|
215
|
+
permissionDecision: 'deny' as const,
|
|
216
|
+
permissionDecisionReason: wholeDiskSearchMessage(what, DATA_DIR),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
201
221
|
/** Build the hooks map to pass into SDK query() options.
|
|
202
222
|
* `exemptBashCommand` is the one command the turn was explicitly told to run in the foreground
|
|
203
223
|
* (a `bash` schedule's command) — see forceBackgroundForScripts. */
|
|
@@ -205,6 +225,7 @@ export function buildHooks(opts?: { exemptBashCommand?: string; offload?: Offloa
|
|
|
205
225
|
const offload = opts && 'offload' in opts ? opts.offload : getOffload();
|
|
206
226
|
return {
|
|
207
227
|
PreToolUse: [
|
|
228
|
+
{ matcher: 'Bash|Grep|Glob', hooks: [denyWholeDiskSearch] },
|
|
208
229
|
...(offload ? [{ matcher: 'Bash', hooks: [denyHeavyLocalMedia(offload)] }] : []),
|
|
209
230
|
{ matcher: 'Bash', hooks: [forceBackgroundForScripts(opts?.exemptBashCommand, opts?.durableJobs), ...(opts?.durableJobs ? [denyNativeBackground] : [])] },
|
|
210
231
|
{ matcher: 'mcp__mcp-slack-use__post_slack_.*', hooks: [resolveSlackMentions] },
|
|
@@ -182,6 +182,10 @@ function formatEventBlock(e: EventContext): string {
|
|
|
182
182
|
return `An external event triggered this automation.\nSource: ${e.source}\nEvent data:\n\`\`\`json\n${body}\n\`\`\``;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** Prefix of every session id a scheduled run mints — the marker that a session belongs to the scheduler. */
|
|
186
|
+
export const SCHEDULED_SESSION_PREFIX = 'sched-';
|
|
187
|
+
export const isScheduledSession = (sessionId: string): boolean => sessionId.startsWith(SCHEDULED_SESSION_PREFIX);
|
|
188
|
+
|
|
185
189
|
export async function runSchedule(
|
|
186
190
|
schedule: Schedule,
|
|
187
191
|
onEvent: (ev: object) => void,
|
|
@@ -192,7 +196,7 @@ export async function runSchedule(
|
|
|
192
196
|
): Promise<ScheduleRunSummary> {
|
|
193
197
|
const now = Date.now();
|
|
194
198
|
// Resume reuses the interrupted run's session (same convo) instead of spawning a new one.
|
|
195
|
-
const sessionId = resume ? resume.sessionId :
|
|
199
|
+
const sessionId = resume ? resume.sessionId : `${SCHEDULED_SESSION_PREFIX}${schedule.id}-${now}`;
|
|
196
200
|
const abortController = new AbortController();
|
|
197
201
|
registerRun(sessionId, abortController);
|
|
198
202
|
|
|
@@ -326,6 +330,7 @@ export async function runSchedule(
|
|
|
326
330
|
abortController,
|
|
327
331
|
onPermissionRequest,
|
|
328
332
|
foregroundBashCommand: allowedCmd ?? undefined,
|
|
333
|
+
scheduled: true,
|
|
329
334
|
})) {
|
|
330
335
|
onEvent({ type: 'session_stream', sessionId, event: ev });
|
|
331
336
|
if (ev.type === 'text_delta') {
|
|
@@ -44,6 +44,8 @@ function isFullEntry(entry: McpEntry): entry is McpFullEntry {
|
|
|
44
44
|
|
|
45
45
|
export interface AgentSettings {
|
|
46
46
|
model?: string;
|
|
47
|
+
/** Model for scheduled (unattended) runs whose prompt names no `[model:…]`. Unset = `model`. */
|
|
48
|
+
scheduleModel?: string;
|
|
47
49
|
engine?: string;
|
|
48
50
|
systemPrompt?: string;
|
|
49
51
|
allowedTools?: string[];
|
package/src/server/skills.ts
CHANGED
|
@@ -320,7 +320,9 @@ export function buildSkillIndexBlock(): string {
|
|
|
320
320
|
const tag = defaultNames.has(name) ? ' (default)' : '';
|
|
321
321
|
lines.push(`- **${name}**${tag}: ${desc}${triggers}${expiry}`);
|
|
322
322
|
}
|
|
323
|
-
|
|
323
|
+
// ABSOLUTE: the agent's cwd is the app dir, not DATA_DIR, so a relative "data/skills/" (or a skill's own
|
|
324
|
+
// `scripts/…`) resolved nowhere and agents fell back to `find /` (3-7 min per call).
|
|
325
|
+
return `<available-skills>\nSkills live in ${SKILLS_DIR}/ — skill X is ${SKILLS_DIR}/X.md, and its bundled files (scripts/, reference/) are under ${SKILLS_DIR}/X/. A relative path in a skill lives under the data dir ${DATA_DIR}/: look in the skill's folder ${SKILLS_DIR}/X/, then ${DATA_DIR}/workspace/, then ${DATA_DIR}/ (skills/… is relative to ${DATA_DIR}/). If you must search, search inside ${DATA_DIR} only — never the whole disk. Use Read to load full skill content when needed.\n${lines.join('\n')}\n</available-skills>`;
|
|
324
326
|
}
|
|
325
327
|
|
|
326
328
|
/* ── Trigger matching ─────────────────────────────────────────────────────────
|