@hyperdrive.bot/paseo-server 0.3.40 → 0.3.41
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/server/server/agent/agent-manager.js +15 -0
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
- package/dist/server/server/agent/agent-storage.d.ts +2 -1
- package/dist/server/server/agent/agent-storage.js +4 -0
- package/dist/server/server/agent/mcp-shared.js +5 -2
- package/dist/server/server/agent/providers/claude/agent.d.ts +34 -0
- package/dist/server/server/agent/providers/claude/agent.js +74 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +7 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +6 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +93 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
- package/dist/server/server/agent/tools/paseo-tools.d.ts +19 -0
- package/dist/server/server/agent/tools/paseo-tools.js +213 -38
- package/dist/server/server/agent/tools/read-only-surface.d.ts +1 -0
- package/dist/server/server/agent/tools/read-only-surface.js +1 -0
- package/dist/server/server/persistence-hooks.js +2 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js → index-cb251ddad56c08c3021036af3a43fc0f.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br → index-cb251ddad56c08c3021036af3a43fc0f.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz → index-cb251ddad56c08c3021036af3a43fc0f.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run tool allowlist enforcement for the claude provider.
|
|
3
|
+
*
|
|
4
|
+
* WHY A HOOK AND NOT `--allowedTools`
|
|
5
|
+
* -----------------------------------
|
|
6
|
+
* `--allowedTools` (CLI) and `options.allowedTools` (SDK) are *permission allow
|
|
7
|
+
* rules*: they pre-approve a tool so it does not prompt. They do not restrict
|
|
8
|
+
* anything. Both of paseo's claude transports also run the child with
|
|
9
|
+
* `--dangerously-skip-permissions` / `allowDangerouslySkipPermissions`, under
|
|
10
|
+
* which every permission rule is moot, so passing an allowlist there would be
|
|
11
|
+
* decorative: accepted, parsed, and silently unenforcing.
|
|
12
|
+
*
|
|
13
|
+
* A `PreToolUse` hook is the one gate that still fires in that state. The Agent
|
|
14
|
+
* SDK says so explicitly (sdk.d.ts, PermissionDeniedHookInput): "PreToolUse hook
|
|
15
|
+
* denies bypass canUseTool". Verified against claude 2.1.239 with
|
|
16
|
+
* `--permission-mode acceptEdits --dangerously-skip-permissions`: a hook exiting
|
|
17
|
+
* 2 blocked a Bash call while an allowlisted Read call went through.
|
|
18
|
+
*
|
|
19
|
+
* FAIL CLOSED
|
|
20
|
+
* -----------
|
|
21
|
+
* When an allowlist is configured, anything this module cannot positively match
|
|
22
|
+
* is DENIED. A matcher that is too strict fails loudly (the model is told which
|
|
23
|
+
* rule set rejected it, and the run keeps going), while a matcher that is too
|
|
24
|
+
* lax fails silently and hands back a guarantee that is not real. Strictness is
|
|
25
|
+
* the safe direction, so unknown rule shapes and unknown specifier sources deny.
|
|
26
|
+
*
|
|
27
|
+
* Note this is *stricter* than `claude-pool launch --allowedTools`, where tools
|
|
28
|
+
* that never request permission (Read, Grep, TodoWrite, ...) were unaffected by
|
|
29
|
+
* the allowlist. Here the allowlist means exactly what it says: a tool absent
|
|
30
|
+
* from it cannot run at all.
|
|
31
|
+
*/
|
|
32
|
+
/** Tools whose specifier (the `Tool(spec)` argument) we know how to read off the input. */
|
|
33
|
+
export const SPECIFIER_SOURCES = {
|
|
34
|
+
Bash: "command",
|
|
35
|
+
BashOutput: "bash_id",
|
|
36
|
+
Read: "file_path",
|
|
37
|
+
Edit: "file_path",
|
|
38
|
+
Write: "file_path",
|
|
39
|
+
NotebookEdit: "notebook_path",
|
|
40
|
+
WebFetch: "url",
|
|
41
|
+
Glob: "pattern",
|
|
42
|
+
Grep: "pattern",
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Parse CSV-or-array allowlist entries into rules. Entries are the same strings
|
|
46
|
+
* Claude Code permission rules use: `Bash`, `Bash(git:*)`, `Read`,
|
|
47
|
+
* `mcp__server__tool`. Blank entries are dropped.
|
|
48
|
+
*/
|
|
49
|
+
export function parseToolAllowlist(entries) {
|
|
50
|
+
const rules = [];
|
|
51
|
+
for (const raw of entries) {
|
|
52
|
+
for (const piece of raw.split(",")) {
|
|
53
|
+
const entry = piece.trim();
|
|
54
|
+
if (!entry)
|
|
55
|
+
continue;
|
|
56
|
+
const match = /^([^()\s]+)\((.*)\)$/.exec(entry);
|
|
57
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
58
|
+
rules.push({ tool: match[1], specifier: match[2].trim() });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
rules.push({ tool: entry });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rules;
|
|
66
|
+
}
|
|
67
|
+
/** Escape a literal for use inside a RegExp, leaving `*` to be expanded by the caller. */
|
|
68
|
+
export function escapeLiteral(value) {
|
|
69
|
+
return value.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Match a Claude-Code-style specifier pattern against a value.
|
|
73
|
+
*
|
|
74
|
+
* Supported shapes (anything else denies):
|
|
75
|
+
* - `*` any value
|
|
76
|
+
* - `prefix:*` value starts with `prefix` (Claude Code's command-prefix form)
|
|
77
|
+
* - `glob` `*` wildcards, everything else literal, anchored both ends
|
|
78
|
+
*/
|
|
79
|
+
export function specifierMatches(pattern, value) {
|
|
80
|
+
if (pattern === "*")
|
|
81
|
+
return true;
|
|
82
|
+
const prefixForm = /^(.*):\*$/.exec(pattern);
|
|
83
|
+
if (prefixForm?.[1] !== undefined) {
|
|
84
|
+
const prefix = prefixForm[1].trim();
|
|
85
|
+
if (!prefix)
|
|
86
|
+
return false;
|
|
87
|
+
return value === prefix || value.startsWith(`${prefix} `);
|
|
88
|
+
}
|
|
89
|
+
const expanded = pattern.split("*").map(escapeLiteral).join(".*");
|
|
90
|
+
return new RegExp(`^${expanded}$`).test(value);
|
|
91
|
+
}
|
|
92
|
+
/** Read the specifier value a `Tool(spec)` rule compares against, or null when unknown. */
|
|
93
|
+
export function readSpecifierValue(toolName, input) {
|
|
94
|
+
const key = SPECIFIER_SOURCES[toolName];
|
|
95
|
+
if (!key)
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof input !== "object" || input === null)
|
|
98
|
+
return null;
|
|
99
|
+
const value = input[key];
|
|
100
|
+
return typeof value === "string" ? value : null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* True when `toolName` (with `input`) is permitted by `rules`.
|
|
104
|
+
*
|
|
105
|
+
* An EMPTY rule list means "no allowlist configured" and allows everything; the
|
|
106
|
+
* caller is responsible for not installing the guard at all in that case.
|
|
107
|
+
*/
|
|
108
|
+
export function isToolAllowed(toolName, input, rules) {
|
|
109
|
+
if (rules.length === 0)
|
|
110
|
+
return true;
|
|
111
|
+
for (const rule of rules) {
|
|
112
|
+
if (rule.tool !== toolName)
|
|
113
|
+
continue;
|
|
114
|
+
if (rule.specifier === undefined)
|
|
115
|
+
return true;
|
|
116
|
+
const value = readSpecifierValue(toolName, input);
|
|
117
|
+
// Unknown specifier source: we cannot verify the rule, so we do not honor it.
|
|
118
|
+
if (value === null)
|
|
119
|
+
continue;
|
|
120
|
+
if (specifierMatches(rule.specifier, value))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/** The message handed back to the model when a call is blocked. */
|
|
126
|
+
export function formatDenialMessage(toolName, rules) {
|
|
127
|
+
const listed = rules
|
|
128
|
+
.map((rule) => (rule.specifier === undefined ? rule.tool : `${rule.tool}(${rule.specifier})`))
|
|
129
|
+
.join(", ");
|
|
130
|
+
return (`Tool "${toolName}" is blocked by this run's tool allowlist and was not executed. ` +
|
|
131
|
+
`Allowed: ${listed}. Do not retry this tool; achieve the goal with an allowed tool or report that you cannot.`);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=tool-allowlist.js.map
|
|
@@ -8,6 +8,7 @@ import type { TerminalManager } from "../../../terminal/terminal-manager.js";
|
|
|
8
8
|
import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js";
|
|
9
9
|
import type { JudgeRelayGate } from "../judge-relay-gate.js";
|
|
10
10
|
import type { ScheduleService } from "../../schedule/service.js";
|
|
11
|
+
import { type ScheduleCadence } from "@hyperdrive.bot/paseo-protocol/schedule/types";
|
|
11
12
|
import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
|
|
12
13
|
import type { GitHubService } from "../../../services/github-service.js";
|
|
13
14
|
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
|
@@ -62,5 +63,23 @@ export interface PaseoToolHostDependencies {
|
|
|
62
63
|
judgeRelayGate?: JudgeRelayGate;
|
|
63
64
|
logger: Logger;
|
|
64
65
|
}
|
|
66
|
+
export interface ScheduleCreateCadenceArgs {
|
|
67
|
+
at?: string;
|
|
68
|
+
every?: string;
|
|
69
|
+
cron?: string;
|
|
70
|
+
timezone?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface ResolvedScheduleCreateCadence {
|
|
73
|
+
cadence: ScheduleCadence;
|
|
74
|
+
/**
|
|
75
|
+
* `at` is a one-off. It lowers to an `every` cadence whose interval is the
|
|
76
|
+
* distance to the instant, pinned to a single run. An `every` schedule stores
|
|
77
|
+
* the computed `nextRunAt`, so a daemon that was down at that moment fires it
|
|
78
|
+
* late on the next tick, which is what a reminder wants. A pinned cron would
|
|
79
|
+
* instead skip silently to the next calendar match a year away.
|
|
80
|
+
*/
|
|
81
|
+
oneOff: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function resolveScheduleCreateCadence(input: ScheduleCreateCadenceArgs, now: Date): ResolvedScheduleCreateCadence;
|
|
65
84
|
export declare function createPaseoToolCatalog(options: PaseoToolHostDependencies): PaseoToolCatalog;
|
|
66
85
|
//# sourceMappingURL=paseo-tools.d.ts.map
|
|
@@ -141,6 +141,51 @@ function resolveScheduleUpdateCadence(input) {
|
|
|
141
141
|
}
|
|
142
142
|
return undefined;
|
|
143
143
|
}
|
|
144
|
+
// A duration ("90m", "2h30m", "45" seconds) rather than a timestamp. Checked
|
|
145
|
+
// first so `at` can accept both without Date parsing guessing at bare numbers.
|
|
146
|
+
const SCHEDULE_DURATION_ONLY_PATTERN = /^(?:\d+|(?:\d+[smhd])+)$/;
|
|
147
|
+
function resolveScheduleAtInstant(at, now) {
|
|
148
|
+
if (SCHEDULE_DURATION_ONLY_PATTERN.test(at)) {
|
|
149
|
+
return new Date(now.getTime() + parseDurationString(at));
|
|
150
|
+
}
|
|
151
|
+
const parsed = new Date(at);
|
|
152
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
153
|
+
throw new Error(`Invalid at value: ${at}. Use an ISO 8601 instant (2026-08-21T21:30:00-03:00) or a duration from now (90m).`);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
export function resolveScheduleCreateCadence(input, now) {
|
|
158
|
+
const at = normalizeScheduleCadenceArg(input.at);
|
|
159
|
+
const every = normalizeScheduleCadenceArg(input.every);
|
|
160
|
+
const cron = normalizeScheduleCadenceArg(input.cron);
|
|
161
|
+
const timeZone = normalizeScheduleTimeZoneArg(input.timezone);
|
|
162
|
+
const provided = [at, every, cron].filter((value) => value !== undefined);
|
|
163
|
+
if (provided.length !== 1) {
|
|
164
|
+
throw new Error("Specify exactly one of at, every, or cron");
|
|
165
|
+
}
|
|
166
|
+
if (timeZone !== undefined && cron === undefined) {
|
|
167
|
+
throw new Error("timezone can only be used with cron");
|
|
168
|
+
}
|
|
169
|
+
if (at !== undefined) {
|
|
170
|
+
const instant = resolveScheduleAtInstant(at, now);
|
|
171
|
+
const everyMs = instant.getTime() - now.getTime();
|
|
172
|
+
if (everyMs <= 0) {
|
|
173
|
+
throw new Error(`at must be in the future: ${instant.toISOString()} is not after ${now.toISOString()}`);
|
|
174
|
+
}
|
|
175
|
+
return { cadence: { type: "every", everyMs }, oneOff: true };
|
|
176
|
+
}
|
|
177
|
+
if (every !== undefined) {
|
|
178
|
+
return { cadence: { type: "every", everyMs: parseDurationString(every) }, oneOff: false };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
cadence: {
|
|
182
|
+
type: "cron",
|
|
183
|
+
expression: cron,
|
|
184
|
+
...(timeZone !== undefined ? { timezone: timeZone } : {}),
|
|
185
|
+
},
|
|
186
|
+
oneOff: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
144
189
|
function resolveScheduleUpdateExpiresAt(input) {
|
|
145
190
|
if (input.expiresIn !== undefined && input.clearExpires) {
|
|
146
191
|
throw new Error("Specify at most one of expiresIn or clearExpires");
|
|
@@ -277,18 +322,6 @@ export function createPaseoToolCatalog(options) {
|
|
|
277
322
|
return tool.handler(await parseToolInput(tool, input), context);
|
|
278
323
|
},
|
|
279
324
|
});
|
|
280
|
-
const buildCronScheduleCadence = (input) => {
|
|
281
|
-
const expression = input.cron?.trim() ?? "";
|
|
282
|
-
if (!expression) {
|
|
283
|
-
throw new Error("cron is required");
|
|
284
|
-
}
|
|
285
|
-
const timezone = normalizeScheduleTimeZoneArg(input.timezone);
|
|
286
|
-
return {
|
|
287
|
-
type: "cron",
|
|
288
|
-
expression,
|
|
289
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
325
|
const buildScheduleExpiry = (expiresIn) => {
|
|
293
326
|
return expiresIn === undefined
|
|
294
327
|
? undefined
|
|
@@ -427,6 +460,59 @@ export function createPaseoToolCatalog(options) {
|
|
|
427
460
|
},
|
|
428
461
|
};
|
|
429
462
|
};
|
|
463
|
+
// A schedule bound to an agent that is gone can never fire: the startup sweep
|
|
464
|
+
// completes it on sight. Fail loudly at create time instead of handing back a
|
|
465
|
+
// schedule that reads active and is already dead.
|
|
466
|
+
const assertScheduleTargetAgentExists = async (agentId) => {
|
|
467
|
+
if (agentManager.getAgent(agentId)) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const record = await agentStorage.get(agentId);
|
|
471
|
+
if (!record || record.internal) {
|
|
472
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
473
|
+
}
|
|
474
|
+
if (record.archivedAt) {
|
|
475
|
+
throw new Error(`Agent ${agentId} is archived and cannot be a schedule target`);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const resolveScheduleCreateTarget = async (params) => {
|
|
479
|
+
const target = params.target?.trim();
|
|
480
|
+
// Default stays new-agent so callers written against the narrower schema
|
|
481
|
+
// keep the behaviour they had before target existed.
|
|
482
|
+
if (!target || target === "new-agent") {
|
|
483
|
+
return resolveNewAgentScheduleTarget({
|
|
484
|
+
...(params.provider !== undefined ? { provider: params.provider } : {}),
|
|
485
|
+
...(params.cwd !== undefined ? { cwd: params.cwd } : {}),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (params.provider !== undefined || params.cwd !== undefined) {
|
|
489
|
+
throw new Error("provider and cwd can only be used with target new-agent");
|
|
490
|
+
}
|
|
491
|
+
if (target === "self") {
|
|
492
|
+
if (!callerAgentId) {
|
|
493
|
+
throw new Error('target "self" requires an agent-scoped session');
|
|
494
|
+
}
|
|
495
|
+
resolveCallerAgent();
|
|
496
|
+
return { type: "agent", agentId: callerAgentId };
|
|
497
|
+
}
|
|
498
|
+
await assertScheduleTargetAgentExists(target);
|
|
499
|
+
return { type: "agent", agentId: target };
|
|
500
|
+
};
|
|
501
|
+
// Recurring creates go through createOrReplace so an agent re-registering the
|
|
502
|
+
// same standing schedule refreshes it instead of minting a twin. A one-off must
|
|
503
|
+
// NOT: createOrReplace carries the existing schedule's `runs` forward, and
|
|
504
|
+
// shouldCompleteSchedule counts them against the incoming maxRuns before tick
|
|
505
|
+
// looks at nextRunAt, so replacing a schedule that has already run with
|
|
506
|
+
// maxRuns 1 completes it on the next tick without ever firing. There is also
|
|
507
|
+
// nothing to make idempotent about a one-off: two calls mean two reminders.
|
|
508
|
+
// Covered by schedule/service.test.ts "replacing a schedule that has run with
|
|
509
|
+
// maxRuns 1 buries it without firing".
|
|
510
|
+
const persistNewSchedule = async (input, oneOff) => {
|
|
511
|
+
if (!scheduleService) {
|
|
512
|
+
throw new Error("Schedule service is not configured");
|
|
513
|
+
}
|
|
514
|
+
return oneOff ? scheduleService.create(input) : scheduleService.createOrReplace(input);
|
|
515
|
+
};
|
|
430
516
|
const ProviderModelInputSchema = AgentProviderEnum.trim()
|
|
431
517
|
.refine((value) => value.includes("/"), {
|
|
432
518
|
message: "provider must be provider/model, for example codex/gpt-5.4",
|
|
@@ -1609,39 +1695,80 @@ export function createPaseoToolCatalog(options) {
|
|
|
1609
1695
|
});
|
|
1610
1696
|
registerTool("create_schedule", {
|
|
1611
1697
|
title: "Create schedule",
|
|
1612
|
-
description: "
|
|
1698
|
+
description: "Schedule a prompt for later: once at a specific time (at), on a repeating interval (every), or on a cron cadence (cron). The prompt can start a fresh agent, come back to you (target self), or go to another agent by id. Prefer this over an in-session loop whenever the work must survive this session ending, since schedules are owned by the daemon.",
|
|
1613
1699
|
inputSchema: {
|
|
1614
1700
|
prompt: z.string().trim().min(1, "prompt is required"),
|
|
1615
|
-
|
|
1701
|
+
at: z
|
|
1702
|
+
.string()
|
|
1703
|
+
.trim()
|
|
1704
|
+
.min(1)
|
|
1705
|
+
.optional()
|
|
1706
|
+
.describe('One-off delivery time. An ISO 8601 instant ("2026-08-21T21:30:00-03:00") or a duration from now ("90m", "2h30m"). Runs exactly once. Mutually exclusive with every and cron.'),
|
|
1707
|
+
every: z
|
|
1708
|
+
.string()
|
|
1709
|
+
.trim()
|
|
1710
|
+
.min(1)
|
|
1711
|
+
.optional()
|
|
1712
|
+
.describe('Repeating interval, for example "5m", "1h", "2h30m", "1d". Mutually exclusive with at and cron.'),
|
|
1713
|
+
cron: z
|
|
1714
|
+
.string()
|
|
1715
|
+
.trim()
|
|
1716
|
+
.min(1)
|
|
1717
|
+
.optional()
|
|
1718
|
+
.describe('Repeating cron expression, for example "0 9 * * 1-5". Mutually exclusive with at and every.'),
|
|
1616
1719
|
timezone: z
|
|
1617
1720
|
.string()
|
|
1618
1721
|
.trim()
|
|
1619
1722
|
.min(1)
|
|
1620
1723
|
.optional()
|
|
1621
|
-
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
|
1622
|
-
name: z
|
|
1623
|
-
|
|
1624
|
-
|
|
1724
|
+
.describe("IANA time zone for the cron cadence. Requires cron. For example: America/New_York."),
|
|
1725
|
+
name: z
|
|
1726
|
+
.string()
|
|
1727
|
+
.optional()
|
|
1728
|
+
.describe("Reusing a name with the same target refreshes that schedule in place instead of creating a second one."),
|
|
1729
|
+
target: z
|
|
1730
|
+
.string()
|
|
1731
|
+
.trim()
|
|
1732
|
+
.min(1)
|
|
1733
|
+
.optional()
|
|
1734
|
+
.describe('Who receives the prompt: "new-agent" (default) starts a fresh agent each run, "self" delivers to you, or an agent id delivers to that agent.'),
|
|
1735
|
+
provider: AgentProviderEnum.optional().describe("Provider, or provider/model (for example: codex or codex/gpt-5.4). Only valid with target new-agent."),
|
|
1736
|
+
cwd: z.string().optional().describe("Working directory. Only valid with target new-agent."),
|
|
1625
1737
|
maxRuns: z.number().int().positive().optional(),
|
|
1626
1738
|
expiresIn: z.string().optional(),
|
|
1739
|
+
runNow: z
|
|
1740
|
+
.boolean()
|
|
1741
|
+
.optional()
|
|
1742
|
+
.describe("Also fire once immediately on create. Defaults to true for every, false for cron. Not allowed with at."),
|
|
1627
1743
|
},
|
|
1628
1744
|
outputSchema: ScheduleSummarySchema.shape,
|
|
1629
|
-
}, async ({ prompt, cron, timezone, name, provider, cwd, maxRuns, expiresIn }) => {
|
|
1745
|
+
}, async ({ prompt, at, every, cron, timezone, name, target, provider, cwd, maxRuns, expiresIn, runNow, }) => {
|
|
1630
1746
|
if (!scheduleService) {
|
|
1631
1747
|
throw new Error("Schedule service is not configured");
|
|
1632
1748
|
}
|
|
1749
|
+
const { cadence, oneOff } = resolveScheduleCreateCadence({ at, every, cron, timezone }, new Date());
|
|
1750
|
+
if (oneOff && maxRuns !== undefined && maxRuns !== 1) {
|
|
1751
|
+
throw new Error("maxRuns cannot be combined with at: an at schedule runs exactly once");
|
|
1752
|
+
}
|
|
1753
|
+
if (oneOff && runNow === true) {
|
|
1754
|
+
throw new Error("runNow cannot be combined with at: an at schedule runs only at its time");
|
|
1755
|
+
}
|
|
1756
|
+
const resolvedTarget = await resolveScheduleCreateTarget({ target, provider, cwd });
|
|
1633
1757
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
|
1634
|
-
const
|
|
1758
|
+
const scheduleInput = {
|
|
1635
1759
|
prompt: prompt.trim(),
|
|
1636
|
-
cadence
|
|
1637
|
-
|
|
1638
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
1639
|
-
}),
|
|
1640
|
-
target: resolveNewAgentScheduleTarget({ provider, cwd }),
|
|
1760
|
+
cadence,
|
|
1761
|
+
target: resolvedTarget,
|
|
1641
1762
|
...(name?.trim() ? { name: name.trim() } : {}),
|
|
1642
|
-
...(
|
|
1763
|
+
...(oneOff
|
|
1764
|
+
? { maxRuns: 1, runOnCreate: false }
|
|
1765
|
+
: {
|
|
1766
|
+
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
1767
|
+
...(runNow === undefined ? {} : { runOnCreate: runNow }),
|
|
1768
|
+
}),
|
|
1643
1769
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
1644
|
-
}
|
|
1770
|
+
};
|
|
1771
|
+
const schedule = await persistNewSchedule(scheduleInput, oneOff);
|
|
1645
1772
|
return {
|
|
1646
1773
|
content: [],
|
|
1647
1774
|
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
|
|
@@ -1649,22 +1776,43 @@ export function createPaseoToolCatalog(options) {
|
|
|
1649
1776
|
});
|
|
1650
1777
|
registerTool("create_heartbeat", {
|
|
1651
1778
|
title: "Create heartbeat",
|
|
1652
|
-
description: "
|
|
1779
|
+
description: "Schedule a prompt back to yourself: once at a specific time (at), on a repeating interval (every), or on a cron cadence (cron). Equivalent to create_schedule with target self.",
|
|
1653
1780
|
inputSchema: {
|
|
1654
1781
|
prompt: z.string().trim().min(1, "prompt is required"),
|
|
1655
|
-
|
|
1782
|
+
at: z
|
|
1783
|
+
.string()
|
|
1784
|
+
.trim()
|
|
1785
|
+
.min(1)
|
|
1786
|
+
.optional()
|
|
1787
|
+
.describe('One-off delivery time. An ISO 8601 instant ("2026-08-21T21:30:00-03:00") or a duration from now ("90m"). Runs exactly once. Mutually exclusive with every and cron.'),
|
|
1788
|
+
every: z
|
|
1789
|
+
.string()
|
|
1790
|
+
.trim()
|
|
1791
|
+
.min(1)
|
|
1792
|
+
.optional()
|
|
1793
|
+
.describe('Repeating interval, for example "5m", "1h", "2h30m". Mutually exclusive with at and cron.'),
|
|
1794
|
+
cron: z
|
|
1795
|
+
.string()
|
|
1796
|
+
.trim()
|
|
1797
|
+
.min(1)
|
|
1798
|
+
.optional()
|
|
1799
|
+
.describe('Repeating cron expression, for example "0 9 * * 1-5". Mutually exclusive with at and every.'),
|
|
1656
1800
|
timezone: z
|
|
1657
1801
|
.string()
|
|
1658
1802
|
.trim()
|
|
1659
1803
|
.min(1)
|
|
1660
1804
|
.optional()
|
|
1661
|
-
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
|
1805
|
+
.describe("IANA time zone for the cron cadence. Requires cron. For example: America/New_York."),
|
|
1662
1806
|
name: z.string().optional(),
|
|
1663
1807
|
maxRuns: z.number().int().positive().optional(),
|
|
1664
1808
|
expiresIn: z.string().optional(),
|
|
1809
|
+
runNow: z
|
|
1810
|
+
.boolean()
|
|
1811
|
+
.optional()
|
|
1812
|
+
.describe("Also fire once immediately on create. Defaults to true for every, false for cron. Not allowed with at."),
|
|
1665
1813
|
},
|
|
1666
1814
|
outputSchema: ScheduleSummarySchema.shape,
|
|
1667
|
-
}, async ({ prompt, cron, timezone, name, maxRuns, expiresIn }) => {
|
|
1815
|
+
}, async ({ prompt, at, every, cron, timezone, name, maxRuns, expiresIn, runNow }) => {
|
|
1668
1816
|
if (!scheduleService) {
|
|
1669
1817
|
throw new Error("Schedule service is not configured");
|
|
1670
1818
|
}
|
|
@@ -1672,23 +1820,50 @@ export function createPaseoToolCatalog(options) {
|
|
|
1672
1820
|
throw new Error("create_heartbeat requires an agent-scoped session");
|
|
1673
1821
|
}
|
|
1674
1822
|
resolveCallerAgent();
|
|
1823
|
+
const { cadence, oneOff } = resolveScheduleCreateCadence({ at, every, cron, timezone }, new Date());
|
|
1824
|
+
if (oneOff && maxRuns !== undefined && maxRuns !== 1) {
|
|
1825
|
+
throw new Error("maxRuns cannot be combined with at: an at schedule runs exactly once");
|
|
1826
|
+
}
|
|
1827
|
+
if (oneOff && runNow === true) {
|
|
1828
|
+
throw new Error("runNow cannot be combined with at: an at schedule runs only at its time");
|
|
1829
|
+
}
|
|
1675
1830
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
|
1676
|
-
const
|
|
1831
|
+
const scheduleInput = {
|
|
1677
1832
|
prompt: prompt.trim(),
|
|
1678
|
-
cadence
|
|
1679
|
-
cron,
|
|
1680
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
1681
|
-
}),
|
|
1833
|
+
cadence,
|
|
1682
1834
|
target: { type: "agent", agentId: callerAgentId },
|
|
1683
1835
|
...(name?.trim() ? { name: name.trim() } : {}),
|
|
1684
|
-
...(
|
|
1836
|
+
...(oneOff
|
|
1837
|
+
? { maxRuns: 1, runOnCreate: false }
|
|
1838
|
+
: {
|
|
1839
|
+
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
1840
|
+
...(runNow === undefined ? {} : { runOnCreate: runNow }),
|
|
1841
|
+
}),
|
|
1685
1842
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
1686
|
-
}
|
|
1843
|
+
};
|
|
1844
|
+
const schedule = await persistNewSchedule(scheduleInput, oneOff);
|
|
1687
1845
|
return {
|
|
1688
1846
|
content: [],
|
|
1689
1847
|
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
|
|
1690
1848
|
};
|
|
1691
1849
|
});
|
|
1850
|
+
registerTool("run_schedule_once", {
|
|
1851
|
+
title: "Run schedule once",
|
|
1852
|
+
description: "Fire a schedule immediately, out of band. The cadence and the next scheduled run are left untouched, but the run is recorded and counts toward maxRuns.",
|
|
1853
|
+
inputSchema: {
|
|
1854
|
+
id: z.string(),
|
|
1855
|
+
},
|
|
1856
|
+
outputSchema: StoredScheduleSchema.shape,
|
|
1857
|
+
}, async ({ id }) => {
|
|
1858
|
+
if (!scheduleService) {
|
|
1859
|
+
throw new Error("Schedule service is not configured");
|
|
1860
|
+
}
|
|
1861
|
+
const schedule = await scheduleService.runOnce(id);
|
|
1862
|
+
return {
|
|
1863
|
+
content: [],
|
|
1864
|
+
structuredContent: ensureValidJson(schedule),
|
|
1865
|
+
};
|
|
1866
|
+
});
|
|
1692
1867
|
registerTool("list_schedules", {
|
|
1693
1868
|
title: "List schedules",
|
|
1694
1869
|
description: "List all schedules managed by the daemon.",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* - `create_terminal`, `kill_terminal`, `send_terminal_keys` arbitrary command execution.
|
|
43
43
|
* - `create_schedule`, `update_schedule`, `delete_schedule`,
|
|
44
44
|
* `pause_schedule`, `resume_schedule`, `create_heartbeat` persistent side effects.
|
|
45
|
+
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
45
46
|
* - `rename_workspace` mutates.
|
|
46
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
47
48
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* - `create_terminal`, `kill_terminal`, `send_terminal_keys` arbitrary command execution.
|
|
43
43
|
* - `create_schedule`, `update_schedule`, `delete_schedule`,
|
|
44
44
|
* `pause_schedule`, `resume_schedule`, `create_heartbeat` persistent side effects.
|
|
45
|
+
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
45
46
|
* - `rename_workspace` mutates.
|
|
46
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
47
48
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
@@ -41,6 +41,7 @@ export function buildConfigOverrides(record) {
|
|
|
41
41
|
extra: record.config?.extra ?? undefined,
|
|
42
42
|
systemPrompt: record.config?.systemPrompt ?? undefined,
|
|
43
43
|
mcpServers: record.config?.mcpServers ?? undefined,
|
|
44
|
+
allowedTools: record.config?.allowedTools ?? undefined,
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
export function buildSessionConfig(record, options) {
|
|
@@ -58,6 +59,7 @@ export function buildSessionConfig(record, options) {
|
|
|
58
59
|
extra: overrides.extra,
|
|
59
60
|
systemPrompt: overrides.systemPrompt,
|
|
60
61
|
mcpServers: overrides.mcpServers,
|
|
62
|
+
allowedTools: overrides.allowedTools,
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
export function isStoredAgentProviderAvailable(record, validProviders) {
|