@namzu/sdk 45.0.0 → 45.1.0
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/CHANGELOG.md +55 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +12 -2
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +37 -6
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +14 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -1
- package/dist/authorization/shell-lexer.js +19 -6
- package/dist/authorization/shell-lexer.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/directory/types.d.ts +2 -0
- package/dist/directory/types.d.ts.map +1 -1
- package/dist/directory/types.js.map +1 -1
- package/dist/manager/resident/outbox.d.ts +4 -4
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +1 -0
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +10 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +7 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/declined.d.ts +12 -0
- package/dist/runtime/query/declined.d.ts.map +1 -0
- package/dist/runtime/query/declined.js +12 -0
- package/dist/runtime/query/declined.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +3 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +14 -2
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +7 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +6 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
- package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/handoff.js +65 -0
- package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/index.js +1 -0
- package/dist/runtime/query/iteration/phases/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +8 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +3 -2
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +4 -3
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/schedules/cron.d.ts +21 -0
- package/dist/schedules/cron.d.ts.map +1 -0
- package/dist/schedules/cron.js +167 -0
- package/dist/schedules/cron.js.map +1 -0
- package/dist/schedules/describe.d.ts +14 -0
- package/dist/schedules/describe.d.ts.map +1 -0
- package/dist/schedules/describe.js +133 -0
- package/dist/schedules/describe.js.map +1 -0
- package/dist/schedules/errors.d.ts +11 -0
- package/dist/schedules/errors.d.ts.map +1 -0
- package/dist/schedules/errors.js +15 -0
- package/dist/schedules/errors.js.map +1 -0
- package/dist/schedules/evaluate.d.ts +35 -0
- package/dist/schedules/evaluate.d.ts.map +1 -0
- package/dist/schedules/evaluate.js +158 -0
- package/dist/schedules/evaluate.js.map +1 -0
- package/dist/schedules/index.d.ts +11 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +8 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/schedules/next-fire.d.ts +50 -0
- package/dist/schedules/next-fire.d.ts.map +1 -0
- package/dist/schedules/next-fire.js +250 -0
- package/dist/schedules/next-fire.js.map +1 -0
- package/dist/schedules/spec.d.ts +30 -0
- package/dist/schedules/spec.d.ts.map +1 -0
- package/dist/schedules/spec.js +169 -0
- package/dist/schedules/spec.js.map +1 -0
- package/dist/schedules/types.d.ts +144 -0
- package/dist/schedules/types.d.ts.map +1 -0
- package/dist/schedules/types.js +11 -0
- package/dist/schedules/types.js.map +1 -0
- package/dist/schedules/tz.d.ts +44 -0
- package/dist/schedules/tz.d.ts.map +1 -0
- package/dist/schedules/tz.js +141 -0
- package/dist/schedules/tz.js.map +1 -0
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +16 -3
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +48 -4
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtins/browser-url.d.ts +83 -0
- package/dist/tools/builtins/browser-url.d.ts.map +1 -0
- package/dist/tools/builtins/browser-url.js +240 -0
- package/dist/tools/builtins/browser-url.js.map +1 -0
- package/dist/tools/builtins/browser.d.ts +367 -0
- package/dist/tools/builtins/browser.d.ts.map +1 -0
- package/dist/tools/builtins/browser.js +704 -0
- package/dist/tools/builtins/browser.js.map +1 -0
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +15 -0
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/schedules/index.d.ts +5 -0
- package/dist/tools/schedules/index.d.ts.map +1 -0
- package/dist/tools/schedules/index.js +4 -0
- package/dist/tools/schedules/index.js.map +1 -0
- package/dist/tools/schedules/loop-tool.d.ts +14 -0
- package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
- package/dist/tools/schedules/loop-tool.js +81 -0
- package/dist/tools/schedules/loop-tool.js.map +1 -0
- package/dist/tools/schedules/present.d.ts +16 -0
- package/dist/tools/schedules/present.d.ts.map +1 -0
- package/dist/tools/schedules/present.js +69 -0
- package/dist/tools/schedules/present.js.map +1 -0
- package/dist/tools/schedules/prompt-scan.d.ts +17 -0
- package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
- package/dist/tools/schedules/prompt-scan.js +92 -0
- package/dist/tools/schedules/prompt-scan.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts +16 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
- package/dist/tools/schedules/schedule-tool.js +327 -0
- package/dist/tools/schedules/schedule-tool.js.map +1 -0
- package/dist/tools/schedules/types.d.ts +184 -0
- package/dist/tools/schedules/types.d.ts.map +1 -0
- package/dist/tools/schedules/types.js +11 -0
- package/dist/tools/schedules/types.js.map +1 -0
- package/dist/types/authorization/index.d.ts +86 -9
- package/dist/types/authorization/index.d.ts.map +1 -1
- package/dist/types/authorization/index.js +11 -1
- package/dist/types/authorization/index.js.map +1 -1
- package/dist/types/browser/index.d.ts +280 -0
- package/dist/types/browser/index.d.ts.map +1 -0
- package/dist/types/browser/index.js +12 -0
- package/dist/types/browser/index.js.map +1 -0
- package/dist/types/session/events.d.ts +6 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +23 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +9 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +41 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +7 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +18 -2
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +13 -3
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +12 -0
- package/dist/utils/id.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization/gate.ts +14 -2
- package/src/authorization/rules.ts +37 -7
- package/src/authorization/shell-lexer.ts +36 -6
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/directory/types.ts +2 -0
- package/src/prompt/coding-agent-doctrine.ts +1 -0
- package/src/public-runtime.ts +28 -0
- package/src/public-tools.ts +35 -0
- package/src/public-types.ts +50 -0
- package/src/runtime/query/declined.ts +12 -0
- package/src/runtime/query/executor.ts +17 -1
- package/src/runtime/query/index.ts +1 -0
- package/src/runtime/query/iteration/index.ts +8 -0
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/phases/handoff.ts +74 -0
- package/src/runtime/query/iteration/phases/index.ts +1 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +11 -4
- package/src/runtime/query/resume-pending.ts +3 -2
- package/src/runtime/query/review-policy.ts +4 -3
- package/src/schedules/cron.ts +202 -0
- package/src/schedules/describe.ts +138 -0
- package/src/schedules/errors.ts +15 -0
- package/src/schedules/evaluate.ts +178 -0
- package/src/schedules/index.ts +18 -0
- package/src/schedules/next-fire.ts +257 -0
- package/src/schedules/spec.ts +210 -0
- package/src/schedules/types.ts +163 -0
- package/src/schedules/tz.ts +155 -0
- package/src/skills/index.ts +1 -1
- package/src/skills/loader.ts +55 -4
- package/src/tools/builtins/browser-url.ts +251 -0
- package/src/tools/builtins/browser.ts +817 -0
- package/src/tools/builtins/skill.ts +18 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/tools/schedules/index.ts +4 -0
- package/src/tools/schedules/loop-tool.ts +85 -0
- package/src/tools/schedules/present.ts +86 -0
- package/src/tools/schedules/prompt-scan.ts +96 -0
- package/src/tools/schedules/schedule-tool.ts +376 -0
- package/src/tools/schedules/types.ts +197 -0
- package/src/types/authorization/index.ts +60 -2
- package/src/types/browser/index.ts +341 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +10 -0
- package/src/types/tool/index.ts +42 -0
- package/src/types/tool/presentation.ts +7 -0
- package/src/utils/frontmatter.ts +29 -3
- package/src/utils/id.ts +14 -0
|
@@ -256,6 +256,24 @@ export const SkillTool = defineTool({
|
|
|
256
256
|
destructive: false,
|
|
257
257
|
concurrencySafe: true,
|
|
258
258
|
|
|
259
|
+
// The body is instructions for the model, often a hundred lines; the
|
|
260
|
+
// person needs the row that says which skill was read, not the text.
|
|
261
|
+
presentCall(input: SkillInput) {
|
|
262
|
+
const name = typeof input?.name === 'string' ? input.name : undefined
|
|
263
|
+
return {
|
|
264
|
+
kind: 'generic',
|
|
265
|
+
presentation: 'activity',
|
|
266
|
+
label:
|
|
267
|
+
name === undefined
|
|
268
|
+
? input?.cursor === undefined
|
|
269
|
+
? 'List skills'
|
|
270
|
+
: 'List more skills'
|
|
271
|
+
: `Read skill ${name}${input.cursor === undefined ? '' : ' (continued)'}`,
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
presentResult: (_input: SkillInput, result) =>
|
|
275
|
+
result.success ? { kind: 'generic', label: 'read', visibility: 'hidden' } : undefined,
|
|
276
|
+
|
|
259
277
|
async execute(input: SkillInput, context) {
|
|
260
278
|
if (!context.skills) {
|
|
261
279
|
return {
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
65
65
|
commandDialect?: ToolDefinition['commandDialect']
|
|
66
66
|
/** The argument holding a filesystem path; see {@link ToolDefinition.pathArgument}. */
|
|
67
67
|
pathArgument?: string
|
|
68
|
+
/** The argument holding a canonical URL; see {@link ToolDefinition.urlArgument}. */
|
|
69
|
+
urlArgument?: string
|
|
68
70
|
/** The argument asking to leave the sandbox; see {@link ToolDefinition.sandboxEscapeArgument}. */
|
|
69
71
|
sandboxEscapeArgument?: string
|
|
70
72
|
execute(input: z.infer<S>, context: ToolContext): Promise<ToolResult>
|
|
@@ -117,6 +119,7 @@ export function defineTool<S extends z.ZodType>(
|
|
|
117
119
|
...(options.commandArgument !== undefined ? { commandArgument: options.commandArgument } : {}),
|
|
118
120
|
...(options.commandDialect !== undefined ? { commandDialect: options.commandDialect } : {}),
|
|
119
121
|
...(options.pathArgument !== undefined ? { pathArgument: options.pathArgument } : {}),
|
|
122
|
+
...(options.urlArgument !== undefined ? { urlArgument: options.urlArgument } : {}),
|
|
120
123
|
...(options.sandboxEscapeArgument !== undefined
|
|
121
124
|
? { sandboxEscapeArgument: options.sandboxEscapeArgument }
|
|
122
125
|
: {}),
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
3
|
+
import { defineTool } from '../defineTool.js'
|
|
4
|
+
import { presentLoopCall, presentLoopResult } from './present.js'
|
|
5
|
+
import type { SessionLoopHost } from './types.js'
|
|
6
|
+
|
|
7
|
+
export const SESSION_LOOP_TOOL_NAME = 'session_loop'
|
|
8
|
+
|
|
9
|
+
const inputSchema = z.object({
|
|
10
|
+
action: z.enum(['create', 'list', 'delete']),
|
|
11
|
+
interval: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('create: how often, e.g. "5m", "1h", or a five-field cron expression'),
|
|
15
|
+
prompt: z.string().min(1).max(4_000).optional().describe('create: the message to send each time'),
|
|
16
|
+
id: z.string().optional().describe('delete: the loop id, or "all"'),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `session_loop` tool: re-send a prompt to THIS conversation on an
|
|
21
|
+
* interval, between turns, while the session is open.
|
|
22
|
+
*
|
|
23
|
+
* `create` is an ordinary reviewed call — not exempt from review — so in
|
|
24
|
+
* `prompt` mode the operator sees it before a loop exists; a loop a model
|
|
25
|
+
* created is labelled as such wherever it is shown. `list` and `delete` only
|
|
26
|
+
* read or remove loops.
|
|
27
|
+
*/
|
|
28
|
+
export function buildSessionLoopTools(host: SessionLoopHost): ToolDefinition[] {
|
|
29
|
+
return [
|
|
30
|
+
defineTool({
|
|
31
|
+
name: SESSION_LOOP_TOOL_NAME,
|
|
32
|
+
description:
|
|
33
|
+
'Re-send a prompt to this conversation on an interval while the session stays open (between turns only; stops when the session closes; expires after 7 days). Use only when the user asks for something to repeat. Minimum interval one minute.',
|
|
34
|
+
inputSchema,
|
|
35
|
+
category: 'custom',
|
|
36
|
+
permissions: [],
|
|
37
|
+
readOnly: (input) => input.action !== 'create',
|
|
38
|
+
destructive: false,
|
|
39
|
+
concurrencySafe: false,
|
|
40
|
+
presentCall: presentLoopCall,
|
|
41
|
+
presentResult: presentLoopResult,
|
|
42
|
+
async execute(input) {
|
|
43
|
+
if (input.action === 'list') {
|
|
44
|
+
const loops = host.list()
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
output:
|
|
48
|
+
loops.length === 0
|
|
49
|
+
? 'No loops in this session.'
|
|
50
|
+
: loops.map((l) => `${l.id} · ${l.schedule} · ${l.prompt.slice(0, 80)}`).join('\n'),
|
|
51
|
+
data: { loops },
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (input.action === 'delete') {
|
|
55
|
+
if (!input.id) return { success: false, output: '', error: 'delete needs id (or "all").' }
|
|
56
|
+
const stopped = await host.delete(input.id)
|
|
57
|
+
return stopped === 0
|
|
58
|
+
? { success: false, output: '', error: `No loop has id "${input.id}".` }
|
|
59
|
+
: { success: true, output: `Stopped ${stopped} loop${stopped === 1 ? '' : 's'}.` }
|
|
60
|
+
}
|
|
61
|
+
if (!input.interval || !input.prompt) {
|
|
62
|
+
return { success: false, output: '', error: 'create needs interval and prompt.' }
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const loop = await host.create({
|
|
66
|
+
interval: input.interval,
|
|
67
|
+
prompt: input.prompt,
|
|
68
|
+
createdBy: 'model',
|
|
69
|
+
})
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
output: `Loop ${loop.id} created: ${loop.schedule}. The operator can stop it with /loop stop ${loop.id}.`,
|
|
73
|
+
data: { id: loop.id },
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
output: '',
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
]
|
|
85
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ToolResult } from '../../types/tool/index.js'
|
|
2
|
+
import type { ToolCallView, ToolResultView } from '../../types/tool/presentation.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the schedule tools read to a person: in words, never ids or JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MAX = 80
|
|
9
|
+
|
|
10
|
+
function oneLine(value: unknown): string {
|
|
11
|
+
if (typeof value !== 'string') return ''
|
|
12
|
+
const flat = value.replace(/\s+/g, ' ').trim()
|
|
13
|
+
return flat.length > MAX ? `${flat.slice(0, MAX - 1)}…` : flat
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function activity(label: string): ToolCallView {
|
|
17
|
+
return { kind: 'generic', presentation: 'activity', label }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HIDDEN: ToolResultView = { kind: 'generic', label: '', visibility: 'hidden' }
|
|
21
|
+
|
|
22
|
+
export function presentScheduleCall(input: {
|
|
23
|
+
readonly action?: unknown
|
|
24
|
+
readonly name?: unknown
|
|
25
|
+
readonly when?: unknown
|
|
26
|
+
readonly job?: unknown
|
|
27
|
+
}): ToolCallView {
|
|
28
|
+
switch (input.action) {
|
|
29
|
+
case 'create': {
|
|
30
|
+
const name = oneLine(input.name)
|
|
31
|
+
const when = oneLine(input.when)
|
|
32
|
+
return activity(`Propose scheduled job${name ? ` · ${name}` : ''}${when ? ` · ${when}` : ''}`)
|
|
33
|
+
}
|
|
34
|
+
case 'list':
|
|
35
|
+
return activity('List scheduled jobs')
|
|
36
|
+
case 'pause':
|
|
37
|
+
return activity(`Pause scheduled job · ${oneLine(input.job)}`)
|
|
38
|
+
case 'resume':
|
|
39
|
+
return activity(`Resume scheduled job · ${oneLine(input.job)}`)
|
|
40
|
+
case 'delete':
|
|
41
|
+
return activity(`Delete scheduled job · ${oneLine(input.job)}`)
|
|
42
|
+
default:
|
|
43
|
+
return activity('Scheduled jobs')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The operator answered no on the tool's own screen: not a failure. */
|
|
48
|
+
function operatorCancelled(result: ToolResult): boolean {
|
|
49
|
+
const data = result.data as { cancelled?: unknown } | undefined
|
|
50
|
+
return data?.cancelled === true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function presentScheduleResult(input: unknown, result: ToolResult): ToolResultView {
|
|
54
|
+
if (!result.success && operatorCancelled(result)) {
|
|
55
|
+
const action = (input as { action?: unknown } | null)?.action
|
|
56
|
+
return {
|
|
57
|
+
kind: 'generic',
|
|
58
|
+
outcome: 'cancelled',
|
|
59
|
+
label: action === 'create' ? 'Cancelled — no job was created' : 'Cancelled — nothing changed',
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!result.success) return { kind: 'generic', label: oneLine(result.error) || 'Not done' }
|
|
63
|
+
return HIDDEN
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function presentLoopCall(input: {
|
|
67
|
+
readonly action?: unknown
|
|
68
|
+
readonly interval?: unknown
|
|
69
|
+
readonly prompt?: unknown
|
|
70
|
+
}): ToolCallView {
|
|
71
|
+
switch (input.action) {
|
|
72
|
+
case 'create':
|
|
73
|
+
return activity(`Loop ${oneLine(input.interval)} · ${oneLine(input.prompt)}`)
|
|
74
|
+
case 'list':
|
|
75
|
+
return activity('List loops')
|
|
76
|
+
case 'delete':
|
|
77
|
+
return activity('Stop loop')
|
|
78
|
+
default:
|
|
79
|
+
return activity('Loops')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function presentLoopResult(_input: unknown, result: ToolResult): ToolResultView {
|
|
84
|
+
if (!result.success) return { kind: 'generic', label: oneLine(result.error) || 'Not done' }
|
|
85
|
+
return HIDDEN
|
|
86
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tripwire over a scheduled prompt, run when the job is proposed.
|
|
3
|
+
*
|
|
4
|
+
* A scheduled prompt runs later with nobody watching, so a prompt that hides
|
|
5
|
+
* characters, tells the model to ignore its instructions, or reads secrets and
|
|
6
|
+
* sends them somewhere deserves a second look before anyone confirms it.
|
|
7
|
+
* Findings are shown to the person confirming; they never block on their own,
|
|
8
|
+
* because the operator may mean exactly what they wrote.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Characters that render as nothing or reorder what is shown. */
|
|
12
|
+
/** Invisible and direction-changing code point ranges, inclusive. */
|
|
13
|
+
const INVISIBLE_RANGES: readonly (readonly [number, number])[] = [
|
|
14
|
+
[0x200b, 0x200f],
|
|
15
|
+
[0x202a, 0x202e],
|
|
16
|
+
[0x2060, 0x2064],
|
|
17
|
+
[0x2066, 0x2069],
|
|
18
|
+
[0xfeff, 0xfeff],
|
|
19
|
+
[0x00ad, 0x00ad],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function isInvisible(code: number): boolean {
|
|
23
|
+
return INVISIBLE_RANGES.some(([lo, hi]) => code >= lo && code <= hi)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasInvisible(text: string): boolean {
|
|
27
|
+
for (const ch of text) if (isInvisible(ch.codePointAt(0) ?? 0)) return true
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** C0 controls other than tab, newline and carriage return, and DEL. */
|
|
32
|
+
function isControl(code: number): boolean {
|
|
33
|
+
return (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) || code === 0x7f
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasControl(text: string): boolean {
|
|
37
|
+
for (let i = 0; i < text.length; i++) if (isControl(text.charCodeAt(i))) return true
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DIRECTIVES: readonly RegExp[] = [
|
|
42
|
+
/\bignore (all |any )?(the )?(previous|prior|above|earlier) (instructions|rules|messages)\b/i,
|
|
43
|
+
/\bdisregard (all |any )?(the )?(previous|prior|above|system) /i,
|
|
44
|
+
/\b(reveal|print|show|repeat) (your|the) (system prompt|instructions)\b/i,
|
|
45
|
+
/\byou are no longer\b/i,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const SECRET_READS: readonly RegExp[] = [
|
|
49
|
+
/~?\/?\.ssh\b|\bid_(rsa|ed25519|ecdsa)\b/i,
|
|
50
|
+
/\.aws\/credentials|\.config\/gcloud|\.kube\/config|\.netrc\b|\.npmrc\b|\.pypirc\b/i,
|
|
51
|
+
/\bcredentials\.json\b|\.env\b/i,
|
|
52
|
+
/\b(printenv|env)\b\s*($|[|;&>])/i,
|
|
53
|
+
/\b[A-Z0-9_]*(API_KEY|SECRET|TOKEN|PASSWORD)\b/,
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const EXFILTRATION: readonly RegExp[] = [
|
|
57
|
+
/\b(curl|wget)\b[^\n|]*\|\s*(ba|z|da)?sh\b/i,
|
|
58
|
+
/\bcurl\b[^\n]*\s(-d|--data(-binary|-raw)?|-F|--form|-T|--upload-file|-X\s*(POST|PUT))\b/i,
|
|
59
|
+
/\b(nc|ncat|netcat|socat)\b\s+\S+\s+\d+/i,
|
|
60
|
+
/\bbase64\b[^\n]*\|\s*(curl|wget|nc)\b/i,
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
/** What the scan found, as sentences for the confirmation screen. Empty when nothing. */
|
|
64
|
+
export function scanSchedulePrompt(prompt: string): string[] {
|
|
65
|
+
const findings: string[] = []
|
|
66
|
+
if (hasInvisible(prompt)) {
|
|
67
|
+
findings.push('The prompt contains invisible or direction-changing characters.')
|
|
68
|
+
}
|
|
69
|
+
if (hasControl(prompt)) findings.push('The prompt contains control characters.')
|
|
70
|
+
if (DIRECTIVES.some((re) => re.test(prompt))) {
|
|
71
|
+
findings.push('The prompt tells the model to ignore or reveal its instructions.')
|
|
72
|
+
}
|
|
73
|
+
if (SECRET_READS.some((re) => re.test(prompt))) {
|
|
74
|
+
findings.push('The prompt mentions credentials, keys or secret files.')
|
|
75
|
+
}
|
|
76
|
+
if (EXFILTRATION.some((re) => re.test(prompt))) {
|
|
77
|
+
findings.push('The prompt pipes a download into a shell or sends data to a remote address.')
|
|
78
|
+
}
|
|
79
|
+
return findings
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The prompt with every invisible and control character made visible as
|
|
84
|
+
* `<U+XXXX>`, so the person confirming sees what the model will read.
|
|
85
|
+
*/
|
|
86
|
+
export function revealHiddenCharacters(prompt: string): string {
|
|
87
|
+
let out = ''
|
|
88
|
+
for (const ch of prompt) {
|
|
89
|
+
const code = ch.codePointAt(0) ?? 0
|
|
90
|
+
out +=
|
|
91
|
+
isControl(code) || isInvisible(code)
|
|
92
|
+
? `<U+${code.toString(16).toUpperCase().padStart(4, '0')}>`
|
|
93
|
+
: ch
|
|
94
|
+
}
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js'
|
|
3
|
+
import { canonicalizeBrowserSitePattern } from '../builtins/browser-url.js'
|
|
4
|
+
import { defineTool } from '../defineTool.js'
|
|
5
|
+
import { presentScheduleCall, presentScheduleResult } from './present.js'
|
|
6
|
+
import { scanSchedulePrompt } from './prompt-scan.js'
|
|
7
|
+
import type {
|
|
8
|
+
ScheduleBrowserGrant,
|
|
9
|
+
ScheduleBrowserSiteLevel,
|
|
10
|
+
ScheduleJobDraft,
|
|
11
|
+
ScheduleToolHost,
|
|
12
|
+
} from './types.js'
|
|
13
|
+
|
|
14
|
+
export const SCHEDULE_TOOL_NAME = 'schedule'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A person reads the whole proposal — the prompt, the rules, the schedule,
|
|
18
|
+
* the credential source — before answering `create`, `resume` or `delete`.
|
|
19
|
+
* The executor's `DEFAULT_TOOL_TIMEOUT_MS` (two minutes) is sized for a tool
|
|
20
|
+
* call, not a person on the other end of a screen, and would abandon the
|
|
21
|
+
* call out from under them mid-read. `save_skill`
|
|
22
|
+
* (`packages/cli/src/skills/save.ts`) waits on the same kind of answer and
|
|
23
|
+
* uses the same thirty minutes, for the same reason.
|
|
24
|
+
*/
|
|
25
|
+
const OPERATOR_CONFIRM_TIMEOUT_MS = 30 * 60_000
|
|
26
|
+
|
|
27
|
+
const EFFECT = z.enum(['allow', 'ask', 'deny'])
|
|
28
|
+
const NETWORK_TOOLS = ['web_fetch', 'web_search']
|
|
29
|
+
const BROWSER_PROFILE = /^[a-z0-9][a-z0-9-]{0,62}$/
|
|
30
|
+
|
|
31
|
+
const inputSchema = z.object({
|
|
32
|
+
action: z
|
|
33
|
+
.enum(['create', 'list', 'pause', 'resume', 'delete'])
|
|
34
|
+
.describe('What to do. create, resume and delete ask the operator first.'),
|
|
35
|
+
name: z
|
|
36
|
+
.string()
|
|
37
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,62}$/)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('create: job name, lowercase letters, digits and dashes'),
|
|
40
|
+
prompt: z.string().min(1).max(20_000).optional().describe('create: what the run is asked to do'),
|
|
41
|
+
when: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('create: "every 30m", "0 9 * * 1-5" (cron), "at 2026-09-24 09:00", "in 2h"'),
|
|
45
|
+
folder: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("create: folder to run in; leave unset for the session's unless the user named one"),
|
|
49
|
+
tz: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(
|
|
53
|
+
"create: IANA time zone; leave unset for the operator's own zone unless the user named another",
|
|
54
|
+
),
|
|
55
|
+
permissions: z
|
|
56
|
+
.object({
|
|
57
|
+
preset: z
|
|
58
|
+
.enum(['read-only', 'edit-in-folder'])
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(
|
|
61
|
+
'read-only: read/glob/grep/ls only. edit-in-folder: also edit and write, bash asks',
|
|
62
|
+
),
|
|
63
|
+
unmatched: z
|
|
64
|
+
.enum(['park', 'deny'])
|
|
65
|
+
.describe('A call no rule covers: park (wait for the operator) or deny'),
|
|
66
|
+
execution: z
|
|
67
|
+
.enum(['host', 'sandbox'])
|
|
68
|
+
.optional()
|
|
69
|
+
.describe(
|
|
70
|
+
'Where commands run; leave unset (this machine) unless the user asked for a sandbox',
|
|
71
|
+
),
|
|
72
|
+
rules: z
|
|
73
|
+
.record(z.string(), z.union([EFFECT, z.record(z.string(), EFFECT)]))
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('Extra rules, e.g. {"bash": {"npm test*": "allow"}}'),
|
|
76
|
+
browser: z
|
|
77
|
+
.object({
|
|
78
|
+
profile: z
|
|
79
|
+
.string()
|
|
80
|
+
.regex(BROWSER_PROFILE)
|
|
81
|
+
.describe('Browser profile the operator signed in with, e.g. "work"'),
|
|
82
|
+
sites: z
|
|
83
|
+
.record(z.string(), z.enum(['read', 'ask', 'act']))
|
|
84
|
+
.describe(
|
|
85
|
+
'Site to level, e.g. {"https://github.com": "read"}. read: open and read only; ask: changes wait for the operator; act: changes run unasked. Unlisted sites are denied; there is no "*".',
|
|
86
|
+
),
|
|
87
|
+
headed: z.boolean().optional().describe('Show the browser window; default no window'),
|
|
88
|
+
})
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Browser access; omit for none'),
|
|
91
|
+
})
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('create: REQUIRED explicit permission set; there is no default'),
|
|
94
|
+
budget: z
|
|
95
|
+
.object({
|
|
96
|
+
maxIterations: z
|
|
97
|
+
.number()
|
|
98
|
+
.int()
|
|
99
|
+
.positive()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe(
|
|
102
|
+
'Model steps one run may take (each model call with its tool calls is one step), not how many times the job runs. Omit for the default; a browser task takes 10 or more.',
|
|
103
|
+
),
|
|
104
|
+
tokenBudget: z
|
|
105
|
+
.number()
|
|
106
|
+
.int()
|
|
107
|
+
.positive()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe(
|
|
110
|
+
'Tokens one run may spend in total. Every model call resends the whole prompt (often 10,000-30,000 tokens each), so a run needs far more than its answer; omit for the default.',
|
|
111
|
+
),
|
|
112
|
+
timeoutMs: z
|
|
113
|
+
.number()
|
|
114
|
+
.int()
|
|
115
|
+
.positive()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('Wall clock of one run, in milliseconds'),
|
|
118
|
+
})
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('Limits of ONE run; omit to use the defaults'),
|
|
121
|
+
job: z.string().optional().describe('pause/resume/delete: job name'),
|
|
122
|
+
allFolders: z.boolean().optional().describe('list: include jobs of other folders (names only)'),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
type Input = z.infer<typeof inputSchema>
|
|
126
|
+
|
|
127
|
+
function refuse(error: string): ToolResult {
|
|
128
|
+
return { success: false, output: '', error }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function effectsOf(rule: unknown): string[] {
|
|
132
|
+
if (typeof rule === 'string') return [rule]
|
|
133
|
+
if (rule && typeof rule === 'object') return Object.values(rule as Record<string, string>)
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Refuse a network tool beside a shell that runs on the host: a scheduled run
|
|
139
|
+
* that can both read the machine and reach the internet, unattended, is the
|
|
140
|
+
* exfiltration shape, and only the operator can choose it.
|
|
141
|
+
*/
|
|
142
|
+
function networkWithHostShell(draft: ScheduleJobDraft): boolean {
|
|
143
|
+
const rules = draft.permissions.rules ?? {}
|
|
144
|
+
const network =
|
|
145
|
+
draft.permissions.browser !== undefined ||
|
|
146
|
+
NETWORK_TOOLS.some((t) => effectsOf(rules[t]).some((e) => e === 'allow' || e === 'ask'))
|
|
147
|
+
if (!network) return false
|
|
148
|
+
const bashEffects = effectsOf(rules.bash)
|
|
149
|
+
// The read-only preset denies bash; its rules are expanded by the host,
|
|
150
|
+
// so the draft carries only its name.
|
|
151
|
+
const shellPossible =
|
|
152
|
+
bashEffects.length > 0
|
|
153
|
+
? bashEffects.some((e) => e !== 'deny')
|
|
154
|
+
: draft.permissions.preset !== 'read-only' && draft.permissions.unmatched !== 'deny'
|
|
155
|
+
return shellPossible && (draft.permissions.execution ?? 'host') === 'host'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The browser grant with its site keys canonicalised, or why it is refused.
|
|
160
|
+
* Keys are rewritten so the host, the confirmation and the compiled rules
|
|
161
|
+
* all see one spelling of each site.
|
|
162
|
+
*/
|
|
163
|
+
function browserGrant(
|
|
164
|
+
host: ScheduleToolHost,
|
|
165
|
+
proposed: NonNullable<NonNullable<Input['permissions']>['browser']>,
|
|
166
|
+
): { ok: true; grant: ScheduleBrowserGrant } | { ok: false; error: string } {
|
|
167
|
+
if (host.browserGrants !== true) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error:
|
|
171
|
+
'This host cannot give a scheduled job browser access. Propose the job without permissions.browser, or ask the operator to add it with `namzu schedule add`.',
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const entries = Object.entries(proposed.sites)
|
|
175
|
+
if (entries.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error:
|
|
179
|
+
'permissions.browser.sites is empty; list each site the run may open, e.g. {"https://github.com": "read"}.',
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const sites: Record<string, ScheduleBrowserSiteLevel> = {}
|
|
183
|
+
for (const [key, level] of entries) {
|
|
184
|
+
if (key.trim() === '*') {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error:
|
|
188
|
+
'A scheduled job cannot grant every site ("*"); unlisted sites are always denied. List each site.',
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const verdict = canonicalizeBrowserSitePattern(key)
|
|
192
|
+
if (!verdict.ok) return { ok: false, error: `permissions.browser.sites: ${verdict.reason}` }
|
|
193
|
+
if (verdict.pattern in sites && sites[verdict.pattern] !== level) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: `permissions.browser.sites names ${verdict.pattern} twice with different levels.`,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
sites[verdict.pattern] = level
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
grant: {
|
|
204
|
+
profile: proposed.profile,
|
|
205
|
+
sites,
|
|
206
|
+
...(proposed.headed !== undefined ? { headed: proposed.headed } : {}),
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function create(
|
|
212
|
+
host: ScheduleToolHost,
|
|
213
|
+
input: Input,
|
|
214
|
+
signal: AbortSignal | undefined,
|
|
215
|
+
): Promise<ToolResult> {
|
|
216
|
+
const missing = (['name', 'prompt', 'when', 'permissions'] as const).filter(
|
|
217
|
+
(k) => input[k] === undefined,
|
|
218
|
+
)
|
|
219
|
+
if (missing.length > 0) {
|
|
220
|
+
return refuse(
|
|
221
|
+
`create needs ${missing.join(', ')}. permissions is required: propose an explicit set (a preset and/or rules, plus unmatched).`,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
const proposed = input.permissions as NonNullable<Input['permissions']>
|
|
225
|
+
if (!proposed.preset && !proposed.rules && !proposed.browser) {
|
|
226
|
+
return refuse(
|
|
227
|
+
'permissions needs a preset, rules or a browser grant; an empty permission set is not a choice.',
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
let permissions: ScheduleJobDraft['permissions'] = proposed
|
|
231
|
+
if (proposed.browser) {
|
|
232
|
+
const grant = browserGrant(host, proposed.browser)
|
|
233
|
+
if (!grant.ok) return refuse(grant.error)
|
|
234
|
+
permissions = { ...proposed, browser: grant.grant }
|
|
235
|
+
}
|
|
236
|
+
const draft: ScheduleJobDraft = {
|
|
237
|
+
name: input.name as string,
|
|
238
|
+
prompt: input.prompt as string,
|
|
239
|
+
when: input.when as string,
|
|
240
|
+
...(input.folder !== undefined ? { folder: input.folder } : {}),
|
|
241
|
+
...(input.tz !== undefined ? { tz: input.tz } : {}),
|
|
242
|
+
permissions,
|
|
243
|
+
...(input.budget ? { budget: input.budget } : {}),
|
|
244
|
+
}
|
|
245
|
+
if (networkWithHostShell(draft)) {
|
|
246
|
+
return refuse(
|
|
247
|
+
'A scheduled job proposed here cannot combine web or browser access with a shell on the host. Deny bash, use execution "sandbox", or ask the operator to create it with `namzu schedule add`.',
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
let preview: Awaited<ReturnType<ScheduleToolHost['preview']>>
|
|
251
|
+
try {
|
|
252
|
+
preview = await host.preview(draft)
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return refuse(error instanceof Error ? error.message : String(error))
|
|
255
|
+
}
|
|
256
|
+
let answer: string
|
|
257
|
+
try {
|
|
258
|
+
answer = await host.confirm(
|
|
259
|
+
{
|
|
260
|
+
preview,
|
|
261
|
+
promptFindings: scanSchedulePrompt(preview.prompt),
|
|
262
|
+
proposedBy: 'model',
|
|
263
|
+
},
|
|
264
|
+
signal,
|
|
265
|
+
)
|
|
266
|
+
} catch {
|
|
267
|
+
answer = 'cancel'
|
|
268
|
+
}
|
|
269
|
+
// The deadline or the turn can settle `host.confirm` from underneath a
|
|
270
|
+
// host that resolves optimistically on its own close; never create on an
|
|
271
|
+
// answer that arrived after the operator was no longer being asked.
|
|
272
|
+
if (signal?.aborted) answer = 'cancel'
|
|
273
|
+
if (answer !== 'create' && answer !== 'create-paused') {
|
|
274
|
+
return {
|
|
275
|
+
success: false,
|
|
276
|
+
output: '',
|
|
277
|
+
error: 'The operator did not confirm the job, so it was not created.',
|
|
278
|
+
data: { cancelled: true },
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const created = await host.create(draft, preview, { paused: answer === 'create-paused' })
|
|
282
|
+
const said =
|
|
283
|
+
answer === 'create-paused'
|
|
284
|
+
? `Job "${created.name}" was created paused. The operator can resume it with /schedule.`
|
|
285
|
+
: `Job "${created.name}" was created. It runs ${preview.schedule}, with nobody watching; results arrive as a notification and a session.`
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
output: created.note ? `${said} ${created.note}` : said,
|
|
289
|
+
data: { name: created.name, paused: answer === 'create-paused' },
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function list(host: ScheduleToolHost, input: Input): Promise<ToolResult> {
|
|
294
|
+
const jobs = await host.list({ allFolders: input.allFolders === true })
|
|
295
|
+
if (jobs.length === 0) return { success: true, output: 'No scheduled jobs.', data: { jobs: [] } }
|
|
296
|
+
const lines = jobs.map(
|
|
297
|
+
(j) =>
|
|
298
|
+
`${j.name} · ${j.state} · ${j.schedule}${j.nextFireAt ? ` · next ${j.nextFireAt}` : ''}${j.lastStatus ? ` · last ${j.lastStatus}` : ''} · ${j.folder}`,
|
|
299
|
+
)
|
|
300
|
+
return { success: true, output: lines.join('\n'), data: { jobs } }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function lifecycle(
|
|
304
|
+
host: ScheduleToolHost,
|
|
305
|
+
input: Input,
|
|
306
|
+
action: 'pause' | 'resume' | 'delete',
|
|
307
|
+
signal: AbortSignal | undefined,
|
|
308
|
+
): Promise<ToolResult> {
|
|
309
|
+
if (!input.job) return refuse(`${action} needs job (the job's name).`)
|
|
310
|
+
const job = await host.find(input.job)
|
|
311
|
+
if (!job) return refuse(`No scheduled job is named "${input.job}".`)
|
|
312
|
+
if (action !== 'pause') {
|
|
313
|
+
let confirmed = false
|
|
314
|
+
try {
|
|
315
|
+
confirmed = await host.confirmAction(job, action, signal)
|
|
316
|
+
} catch {
|
|
317
|
+
confirmed = false
|
|
318
|
+
}
|
|
319
|
+
if (signal?.aborted) confirmed = false
|
|
320
|
+
if (!confirmed)
|
|
321
|
+
return {
|
|
322
|
+
...refuse(`The operator did not confirm; "${job.name}" was not changed.`),
|
|
323
|
+
data: { cancelled: true },
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (action === 'pause') await host.pause(job.name)
|
|
327
|
+
else if (action === 'resume') await host.resume(job.name)
|
|
328
|
+
else await host.delete(job.name)
|
|
329
|
+
const verb = action === 'pause' ? 'paused' : action === 'resume' ? 'resumed' : 'deleted'
|
|
330
|
+
return { success: true, output: `Job "${job.name}" ${verb}.`, data: { name: job.name, action } }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The `schedule` tool: create, list, pause, resume and delete the operator's
|
|
335
|
+
* scheduled jobs from a conversation.
|
|
336
|
+
*
|
|
337
|
+
* Creation, resuming and deleting are confirmed by a person through the host
|
|
338
|
+
* (`ScheduleToolHost.confirm`), on a screen the host draws from its own
|
|
339
|
+
* computation. The model cannot propose that uncovered calls run without
|
|
340
|
+
* asking, nor web access beside a host shell. Register it only where a person
|
|
341
|
+
* is present to confirm: never in a headless run, a scheduled run or a
|
|
342
|
+
* sub-agent.
|
|
343
|
+
*/
|
|
344
|
+
export function buildScheduleTools(host: ScheduleToolHost): ToolDefinition[] {
|
|
345
|
+
return [
|
|
346
|
+
defineTool({
|
|
347
|
+
name: SCHEDULE_TOOL_NAME,
|
|
348
|
+
description:
|
|
349
|
+
"Manage the operator's scheduled jobs: prompts that run later in a folder, with nobody watching, under an explicit permission set. Use it only when the user asks for something to happen on a schedule. create, resume and delete are confirmed by the operator; pause is not. A job needs name, prompt, when and permissions (unmatched: park or deny, plus a preset, rules or a browser grant). Leave every other field (folder, tz, execution, budget, headed) unset unless the user asked for it: the defaults are the operator's, and the confirmation marks each value you chose. Scheduled runs cannot ask questions.",
|
|
350
|
+
inputSchema,
|
|
351
|
+
category: 'custom',
|
|
352
|
+
permissions: [],
|
|
353
|
+
readOnly: (input) => input.action === 'list',
|
|
354
|
+
// `delete` removes a job only after the operator confirms it on the
|
|
355
|
+
// host's own screen; a destructive flag would put a second review,
|
|
356
|
+
// over the raw arguments, in front of that confirmation.
|
|
357
|
+
destructive: false,
|
|
358
|
+
concurrencySafe: false,
|
|
359
|
+
presentCall: presentScheduleCall,
|
|
360
|
+
presentResult: presentScheduleResult,
|
|
361
|
+
// A person, not a tool, answers `create`/`resume`/`delete`; see
|
|
362
|
+
// `OPERATOR_CONFIRM_TIMEOUT_MS`.
|
|
363
|
+
timeoutMs: OPERATOR_CONFIRM_TIMEOUT_MS,
|
|
364
|
+
async execute(input, context: ToolContext) {
|
|
365
|
+
switch (input.action) {
|
|
366
|
+
case 'create':
|
|
367
|
+
return create(host, input, context.abortSignal)
|
|
368
|
+
case 'list':
|
|
369
|
+
return list(host, input)
|
|
370
|
+
default:
|
|
371
|
+
return lifecycle(host, input, input.action, context.abortSignal)
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
]
|
|
376
|
+
}
|