@namzu/sdk 44.3.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 +94 -0
- package/dist/authorization/command-line.d.ts +66 -19
- package/dist/authorization/command-line.d.ts.map +1 -1
- package/dist/authorization/command-line.js +130 -270
- package/dist/authorization/command-line.js.map +1 -1
- package/dist/authorization/gate.d.ts +7 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +13 -3
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts +10 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +55 -8
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +152 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -0
- package/dist/authorization/shell-lexer.js +2156 -0
- package/dist/authorization/shell-lexer.js.map +1 -0
- package/dist/authorization/skill-grant.d.ts +182 -0
- package/dist/authorization/skill-grant.d.ts.map +1 -0
- package/dist/authorization/skill-grant.js +314 -0
- package/dist/authorization/skill-grant.js.map +1 -0
- 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/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +5 -2
- package/dist/persona/assembler.js.map +1 -1
- 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 +5 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +15 -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 +12 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +9 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +2 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +6 -2
- package/dist/runtime/jobs/registry.js.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 +36 -40
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +95 -53
- 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 +8 -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 +13 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +13 -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 +56 -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 +11 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +36 -3
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +3 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.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 +21 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +51 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +18 -6
- package/dist/tools/builtins/bash.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 +2 -9
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +74 -51
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/command-shell.d.ts +90 -0
- package/dist/tools/command-shell.d.ts.map +1 -0
- package/dist/tools/command-shell.js +129 -0
- package/dist/tools/command-shell.js.map +1 -0
- package/dist/tools/defineTool.d.ts +13 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +30 -1
- 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/hitl/index.d.ts +23 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- 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 +108 -5
- 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 +35 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +45 -5
- 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/command-line.ts +148 -293
- package/src/authorization/gate.ts +22 -2
- package/src/authorization/rules.ts +67 -8
- package/src/authorization/shell-lexer.ts +2349 -0
- package/src/authorization/skill-grant.ts +400 -0
- 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/persona/assembler.ts +5 -2
- package/src/prompt/coding-agent-doctrine.ts +1 -0
- package/src/public-runtime.ts +37 -0
- package/src/public-tools.ts +37 -1
- package/src/public-types.ts +57 -0
- package/src/runtime/jobs/registry.ts +19 -11
- package/src/runtime/query/declined.ts +12 -0
- package/src/runtime/query/executor.ts +116 -56
- package/src/runtime/query/index.ts +8 -0
- package/src/runtime/query/iteration/index.ts +13 -0
- package/src/runtime/query/iteration/phases/context.ts +13 -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 +55 -3
- package/src/runtime/query/resume-pending.ts +3 -2
- package/src/runtime/query/review-policy.ts +57 -3
- package/src/runtime/query/tooling.ts +4 -0
- 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 +59 -1
- package/src/tools/builtins/bash.ts +24 -6
- package/src/tools/builtins/browser-url.ts +251 -0
- package/src/tools/builtins/browser.ts +817 -0
- package/src/tools/builtins/skill.ts +92 -53
- package/src/tools/command-shell.ts +166 -0
- package/src/tools/defineTool.ts +36 -1
- 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/hitl/index.ts +21 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +10 -0
- package/src/types/tool/index.ts +109 -5
- package/src/types/tool/presentation.ts +7 -0
- package/src/utils/frontmatter.ts +81 -5
- package/src/utils/id.ts +14 -0
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
|
|
4
|
+
import { parseAllowedTools } from '../../authorization/skill-grant.js'
|
|
4
5
|
import { isInvocableBy, skillInvocation } from '../../types/skills/index.js'
|
|
5
6
|
import { defineTool } from '../defineTool.js'
|
|
6
7
|
|
|
8
|
+
export { parseAllowedTools }
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
|
-
* Load a skill's instructions, and
|
|
11
|
+
* Load a skill's instructions, and apply what its `allowed-tools` grants.
|
|
9
12
|
*
|
|
10
13
|
* The manifest in the system prompt told the model that a SKILL.md exists
|
|
11
14
|
* and to "read the SKILL.md at its <location> before writing code" — which
|
|
12
15
|
* is a filesystem instruction, so a turn with no filesystem tools could see
|
|
13
|
-
* every skill it had and open none of them.
|
|
14
|
-
* it: *"when the runtime exposes filesystem or skill-loading tools"*. There
|
|
15
|
-
* was no skill-loading tool.
|
|
16
|
+
* every skill it had and open none of them. There was no skill-loading tool.
|
|
16
17
|
*
|
|
17
|
-
* `allowed-tools`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* `allowed-tools` is a PRE-APPROVAL, as the Agent Skills format defines it:
|
|
19
|
+
* the listed tools skip the approval prompt for the rest of this turn, and
|
|
20
|
+
* every other tool stays callable under the turn's ordinary review. It was
|
|
21
|
+
* read here for a while as a restriction — the listed tools and nothing
|
|
22
|
+
* else, from the next batch — which inverted what skill authors mean by it
|
|
23
|
+
* and left a model that loaded `allowed-tools: Read Grep` without `bash`.
|
|
24
|
+
* See `authorization/skill-grant.ts` for what a grant can and cannot do.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
const inputSchema = z.object({
|
|
@@ -47,6 +51,8 @@ interface SkillSnapshot {
|
|
|
47
51
|
readonly name: string
|
|
48
52
|
readonly body: string
|
|
49
53
|
readonly allowedTools: readonly string[] | undefined
|
|
54
|
+
/** Bound into the cursor because `${CLAUDE_SKILL_DIR}` in a grant expands to it. */
|
|
55
|
+
readonly skillDirectory: string | undefined
|
|
50
56
|
readonly invocation: ReturnType<typeof skillInvocation>
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -76,6 +82,9 @@ function snapshotDigest(snapshot: SkillSnapshot): string {
|
|
|
76
82
|
name: snapshot.name,
|
|
77
83
|
body: snapshot.body,
|
|
78
84
|
allowedTools: snapshot.allowedTools ?? null,
|
|
85
|
+
...(snapshot.skillDirectory === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { skillDirectory: snapshot.skillDirectory }),
|
|
79
88
|
invocation: snapshot.invocation,
|
|
80
89
|
}),
|
|
81
90
|
)
|
|
@@ -231,27 +240,6 @@ function pageSkillBody(input: {
|
|
|
231
240
|
return undefined
|
|
232
241
|
}
|
|
233
242
|
|
|
234
|
-
/**
|
|
235
|
-
* `allowed-tools` as a list.
|
|
236
|
-
*
|
|
237
|
-
* Comma-separated in the frontmatter because that is what authors write and
|
|
238
|
-
* what the field has always accepted. Split here rather than at parse so
|
|
239
|
-
* the stored metadata keeps the author's own string — the same reasoning
|
|
240
|
-
* `invocation` uses for not defaulting at parse.
|
|
241
|
-
*/
|
|
242
|
-
export function parseAllowedTools(declared: string | undefined): readonly string[] | undefined {
|
|
243
|
-
if (declared === undefined) return undefined
|
|
244
|
-
const names = declared
|
|
245
|
-
.split(',')
|
|
246
|
-
.map((name) => name.trim())
|
|
247
|
-
.filter((name) => name.length > 0)
|
|
248
|
-
// An empty result from a non-empty declaration is a real answer and not
|
|
249
|
-
// the same as "declared nothing": `allowed-tools: ""` is an author
|
|
250
|
-
// saying this skill needs no tools, and collapsing it to `undefined`
|
|
251
|
-
// would silently widen that to everything.
|
|
252
|
-
return names
|
|
253
|
-
}
|
|
254
|
-
|
|
255
243
|
export const SKILL_TOOL_NAME = 'skill'
|
|
256
244
|
|
|
257
245
|
export const SkillTool = defineTool({
|
|
@@ -261,13 +249,31 @@ export const SkillTool = defineTool({
|
|
|
261
249
|
inputSchema,
|
|
262
250
|
category: 'analysis',
|
|
263
251
|
permissions: [],
|
|
264
|
-
// Reads instructions and changes nothing.
|
|
265
|
-
//
|
|
266
|
-
//
|
|
252
|
+
// Reads instructions and changes nothing on disk. What it does change is
|
|
253
|
+
// the turn's approvals, through `grantSkillTools`, and only ever towards
|
|
254
|
+
// fewer prompts for calls the operator's policy already leaves to review.
|
|
267
255
|
readOnly: true,
|
|
268
256
|
destructive: false,
|
|
269
257
|
concurrencySafe: true,
|
|
270
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
|
+
|
|
271
277
|
async execute(input: SkillInput, context) {
|
|
272
278
|
if (!context.skills) {
|
|
273
279
|
return {
|
|
@@ -382,6 +388,7 @@ export const SkillTool = defineTool({
|
|
|
382
388
|
name: input.name,
|
|
383
389
|
body: skill.body ?? '(this skill has no body)',
|
|
384
390
|
allowedTools: allowed,
|
|
391
|
+
skillDirectory: skill.dirPath,
|
|
385
392
|
invocation,
|
|
386
393
|
}
|
|
387
394
|
const digest = snapshotDigest(snapshot)
|
|
@@ -403,10 +410,20 @@ export const SkillTool = defineTool({
|
|
|
403
410
|
start = parsed.offset
|
|
404
411
|
}
|
|
405
412
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
// Compiled before paging so the notice can say what the grant is, and
|
|
414
|
+
// committed only after paging succeeded: a load that fails here gave
|
|
415
|
+
// the model no instructions, so it must not have approved anything.
|
|
416
|
+
// Idempotent: a continuation call grants the same entries again, and
|
|
417
|
+
// the turn's set keeps one copy.
|
|
418
|
+
let grant: ReturnType<NonNullable<typeof context.grantSkillTools>> | undefined
|
|
419
|
+
if (allowed !== undefined && allowed.length > 0 && context.grantSkillTools) {
|
|
420
|
+
grant = context.grantSkillTools({
|
|
421
|
+
skill: skill.metadata.name,
|
|
422
|
+
allowedTools: allowed,
|
|
423
|
+
...(snapshot.skillDirectory ? { skillDirectory: snapshot.skillDirectory } : {}),
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
const notice = grantNotice(allowed, grant, context.grantSkillTools !== undefined)
|
|
410
427
|
const page = pageSkillBody({
|
|
411
428
|
snapshot,
|
|
412
429
|
digest,
|
|
@@ -421,23 +438,7 @@ export const SkillTool = defineTool({
|
|
|
421
438
|
error: `The model-visible tool-output budget is too small to read "${input.name}" safely. Increase maxToolOutputChars and retry.`,
|
|
422
439
|
}
|
|
423
440
|
}
|
|
424
|
-
|
|
425
|
-
// Cursor and policy validation must finish before this mutation. A
|
|
426
|
-
// continuation is bound to the body AND its effective authorization
|
|
427
|
-
// metadata, so an edit to allowed-tools or invocation cannot widen the
|
|
428
|
-
// next batch under an old cursor.
|
|
429
|
-
if (allowed !== undefined) {
|
|
430
|
-
// Adopted, not merely announced. The notice below tells the model
|
|
431
|
-
// what happened; this is what makes it true whether or not the
|
|
432
|
-
// model reads it — the difference between the field as it was and
|
|
433
|
-
// the field as a declaration.
|
|
434
|
-
//
|
|
435
|
-
// Absent `adoptSkillScope`, the notice still goes out and is all
|
|
436
|
-
// there is: a host driving this tool outside a turn has no executor
|
|
437
|
-
// to enforce anything, and saying nothing would be worse than
|
|
438
|
-
// advice.
|
|
439
|
-
context.adoptSkillScope?.({ skill: skill.metadata.name, allowedTools: allowed })
|
|
440
|
-
}
|
|
441
|
+
grant?.commit()
|
|
441
442
|
|
|
442
443
|
return {
|
|
443
444
|
success: true,
|
|
@@ -445,8 +446,46 @@ export const SkillTool = defineTool({
|
|
|
445
446
|
data: {
|
|
446
447
|
skill: skill.metadata.name,
|
|
447
448
|
...(allowed === undefined ? {} : { allowedTools: allowed }),
|
|
449
|
+
...(grant ? { granted: grant.granted, ignored: grant.ignored } : {}),
|
|
448
450
|
...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }),
|
|
449
451
|
},
|
|
450
452
|
}
|
|
451
453
|
},
|
|
452
454
|
})
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* What the model is told about `allowed-tools`.
|
|
458
|
+
*
|
|
459
|
+
* The sentence that matters most is the second one. The old notice said
|
|
460
|
+
* "restrict yourself to", and a model told that does exactly what the owner
|
|
461
|
+
* reported: it stops using `bash` and tries to do the work through the skill.
|
|
462
|
+
* So the notice says, every time, that nothing was taken away.
|
|
463
|
+
*/
|
|
464
|
+
function grantNotice(
|
|
465
|
+
allowed: readonly string[] | undefined,
|
|
466
|
+
grant:
|
|
467
|
+
| {
|
|
468
|
+
granted: readonly string[]
|
|
469
|
+
ignored: readonly { entry: string; reason: string }[]
|
|
470
|
+
}
|
|
471
|
+
| undefined,
|
|
472
|
+
canGrant: boolean,
|
|
473
|
+
): string {
|
|
474
|
+
if (allowed === undefined || allowed.length === 0) return ''
|
|
475
|
+
const unchanged =
|
|
476
|
+
'Every other tool remains available and is reviewed as usual; this skill does not limit which tools you may use.'
|
|
477
|
+
if (!canGrant || !grant) {
|
|
478
|
+
return `\n\n[This skill lists allowed-tools (${allowed.join(', ')}), but this host applies no pre-approval, so those calls are reviewed as usual. ${unchanged}]`
|
|
479
|
+
}
|
|
480
|
+
const lines: string[] = []
|
|
481
|
+
lines.push(
|
|
482
|
+
grant.granted.length > 0
|
|
483
|
+
? `Pre-approved for the rest of this turn: ${grant.granted.join(', ')}. Deny and ask rules, plan and strict mode, and review of destructive calls still apply.`
|
|
484
|
+
: 'Nothing in allowed-tools could be pre-approved.',
|
|
485
|
+
)
|
|
486
|
+
for (const { entry, reason } of grant.ignored) {
|
|
487
|
+
lines.push(`Ignored allowed-tools entry "${entry}": ${reason}.`)
|
|
488
|
+
}
|
|
489
|
+
lines.push(unchanged)
|
|
490
|
+
return `\n\n[${lines.join(' ')}]`
|
|
491
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which shell runs a `bash` tool command, and in what dialect it must be read.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is one decision
|
|
5
|
+
*
|
|
6
|
+
* The permission rules read a command line before it runs
|
|
7
|
+
* (`authorization/shell-lexer.ts`), and a reading is only as good as its
|
|
8
|
+
* match with the shell that runs the line afterwards. The tool is called
|
|
9
|
+
* `bash` and its description says bash, but it used to spawn `/bin/sh -c`:
|
|
10
|
+
* bash on some hosts, `dash` on Debian and Ubuntu, `busybox sh` in small
|
|
11
|
+
* images. `$'\x3b'`, `|&`, `&>` and `<<<` mean different things in those,
|
|
12
|
+
* so a line the rules read as bash could run as something else.
|
|
13
|
+
*
|
|
14
|
+
* So the host path now runs bash wherever bash exists, and the reading
|
|
15
|
+
* follows what was actually chosen:
|
|
16
|
+
*
|
|
17
|
+
* - bash found (or named by `NAMZU_BASH_SHELL`) → `bash -c`, read in the
|
|
18
|
+
* `bash` dialect;
|
|
19
|
+
* - no bash → `/bin/sh -c`, read in the conservative `sh` dialect, in which
|
|
20
|
+
* every construct whose meaning differs between bash and a POSIX shell
|
|
21
|
+
* makes a line opaque;
|
|
22
|
+
* - inside a sandbox the guest image decides, and the rules cannot see it,
|
|
23
|
+
* so a small launcher runs bash when the guest has it and `/bin/sh`
|
|
24
|
+
* otherwise, and the line is always read in the `sh` dialect, which is
|
|
25
|
+
* right for either.
|
|
26
|
+
*
|
|
27
|
+
* ## Equivalent to what ran before
|
|
28
|
+
*
|
|
29
|
+
* `/bin/sh -c` read no startup file. `bash -c` reads none either, except
|
|
30
|
+
* the file `BASH_ENV` names, and it imports shell functions (`BASH_FUNC_*`)
|
|
31
|
+
* and parser options (`SHELLOPTS`, `BASHOPTS`) from its environment. Any of
|
|
32
|
+
* those would change what a command line means after the rules read it, so
|
|
33
|
+
* they are removed from the environment of the spawned bash.
|
|
34
|
+
* `NAMZU_BASH_SHELL=/bin/sh` restores the old shell exactly.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { constants, accessSync } from 'node:fs'
|
|
38
|
+
import { delimiter, join } from 'node:path'
|
|
39
|
+
|
|
40
|
+
import type { ShellDialect } from '../types/tool/index.js'
|
|
41
|
+
|
|
42
|
+
/** The shell a host-side command runs in. */
|
|
43
|
+
export interface CommandShell {
|
|
44
|
+
/** The executable, run as `<path> -c <command>`. Undefined: Node's platform shell (Windows). */
|
|
45
|
+
readonly path: string | undefined
|
|
46
|
+
/** How the permission rules read a line this shell runs. */
|
|
47
|
+
readonly dialect: ShellDialect
|
|
48
|
+
/** Where the choice came from, for diagnostics. */
|
|
49
|
+
readonly source: 'override' | 'bash' | 'sh' | 'platform'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What resolution looks at. Injected by tests to simulate a host without bash. */
|
|
53
|
+
export interface CommandShellProbe {
|
|
54
|
+
readonly env: NodeJS.ProcessEnv
|
|
55
|
+
readonly platform: NodeJS.Platform
|
|
56
|
+
readonly isExecutable: (path: string) => boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const WELL_KNOWN_BASH = ['/bin/bash', '/usr/bin/bash']
|
|
60
|
+
|
|
61
|
+
/** Environment variables that change what a bash command line means. */
|
|
62
|
+
const BASH_STARTUP_VARIABLES = new Set(['BASH_ENV', 'ENV', 'SHELLOPTS', 'BASHOPTS'])
|
|
63
|
+
|
|
64
|
+
export function findCommandShell(probe: CommandShellProbe): CommandShell {
|
|
65
|
+
const override = probe.env.NAMZU_BASH_SHELL
|
|
66
|
+
if (override !== undefined && override !== '') {
|
|
67
|
+
// Read as bash only when it is bash; anything else gets the reading
|
|
68
|
+
// that holds for every POSIX shell.
|
|
69
|
+
const name = override.slice(override.lastIndexOf('/') + 1)
|
|
70
|
+
return { path: override, dialect: name === 'bash' ? 'bash' : 'sh', source: 'override' }
|
|
71
|
+
}
|
|
72
|
+
// Windows keeps Node's platform shell. Looking `bash` up on its PATH can
|
|
73
|
+
// find WSL's launcher, which runs the command in another system.
|
|
74
|
+
if (probe.platform === 'win32') return { path: undefined, dialect: 'sh', source: 'platform' }
|
|
75
|
+
for (const directory of (probe.env.PATH ?? '').split(delimiter)) {
|
|
76
|
+
if (directory === '' || !directory.startsWith('/')) continue
|
|
77
|
+
const candidate = join(directory, 'bash')
|
|
78
|
+
if (probe.isExecutable(candidate)) return { path: candidate, dialect: 'bash', source: 'bash' }
|
|
79
|
+
}
|
|
80
|
+
for (const candidate of WELL_KNOWN_BASH) {
|
|
81
|
+
if (probe.isExecutable(candidate)) return { path: candidate, dialect: 'bash', source: 'bash' }
|
|
82
|
+
}
|
|
83
|
+
return { path: '/bin/sh', dialect: 'sh', source: 'sh' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isExecutable(path: string): boolean {
|
|
87
|
+
try {
|
|
88
|
+
accessSync(path, constants.X_OK)
|
|
89
|
+
return true
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let resolved: CommandShell | undefined
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The host's command shell, resolved once per process. The same value serves
|
|
99
|
+
* the permission rules and the spawn, so the two cannot disagree.
|
|
100
|
+
*/
|
|
101
|
+
export function hostCommandShell(): CommandShell {
|
|
102
|
+
if (resolved === undefined) {
|
|
103
|
+
resolved = findCommandShell({ env: process.env, platform: process.platform, isExecutable })
|
|
104
|
+
}
|
|
105
|
+
return resolved
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Replace the resolved host shell; `undefined` resolves again on next use. For tests. */
|
|
109
|
+
export function setHostCommandShellForTesting(shell: CommandShell | undefined): void {
|
|
110
|
+
resolved = shell
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The spawn for one command on the host: executable, arguments, environment.
|
|
115
|
+
* For bash, the variables that would change the line's meaning are dropped.
|
|
116
|
+
*/
|
|
117
|
+
export function hostShellSpawn(
|
|
118
|
+
command: string,
|
|
119
|
+
env: NodeJS.ProcessEnv,
|
|
120
|
+
shell: CommandShell = hostCommandShell(),
|
|
121
|
+
): {
|
|
122
|
+
readonly file: string | undefined
|
|
123
|
+
readonly args: readonly string[]
|
|
124
|
+
readonly env: NodeJS.ProcessEnv
|
|
125
|
+
} {
|
|
126
|
+
if (shell.path === undefined) return { file: undefined, args: [command], env }
|
|
127
|
+
if (shell.dialect !== 'bash') return { file: shell.path, args: ['-c', command], env }
|
|
128
|
+
return { file: shell.path, args: ['-c', command], env: withoutBashStartup(env) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function withoutBashStartup<T extends Readonly<Record<string, string | undefined>>>(
|
|
132
|
+
env: T,
|
|
133
|
+
): T {
|
|
134
|
+
const out: Record<string, string | undefined> = {}
|
|
135
|
+
for (const [name, value] of Object.entries(env)) {
|
|
136
|
+
if (BASH_STARTUP_VARIABLES.has(name) || name.startsWith('BASH_FUNC_')) continue
|
|
137
|
+
out[name] = value
|
|
138
|
+
}
|
|
139
|
+
return out as T
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The command a sandbox runs for one command line: bash when the guest has
|
|
144
|
+
* it, `/bin/sh` otherwise. The rules read a sandboxed line in the `sh`
|
|
145
|
+
* dialect, which holds for both. The command is passed as an argument, never
|
|
146
|
+
* spliced into the launcher's text.
|
|
147
|
+
*
|
|
148
|
+
* The launcher does not `unset` the startup variables: where `/bin/sh` is
|
|
149
|
+
* bash, `SHELLOPTS` arriving in the environment is readonly and `unset`
|
|
150
|
+
* fails. The guest's environment is an allowlist plus the host's `env`, so
|
|
151
|
+
* callers drop them from that `env` with {@link withoutBashStartup}.
|
|
152
|
+
*/
|
|
153
|
+
export const SANDBOX_SHELL_LAUNCHER =
|
|
154
|
+
'if command -v bash >/dev/null 2>&1; then exec bash -c "$1"; fi; exec /bin/sh -c "$1"'
|
|
155
|
+
|
|
156
|
+
export function sandboxShellSpawn(command: string): {
|
|
157
|
+
readonly file: string
|
|
158
|
+
readonly args: string[]
|
|
159
|
+
} {
|
|
160
|
+
return { file: '/bin/sh', args: ['-c', SANDBOX_SHELL_LAUNCHER, 'sh', command] }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The dialect a `bash` tool command is read in. */
|
|
164
|
+
export function bashToolDialect(context: { readonly sandboxed: boolean }): ShellDialect {
|
|
165
|
+
return context.sandboxed ? 'sh' : hostCommandShell().dialect
|
|
166
|
+
}
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -61,13 +61,46 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
61
61
|
* hand-written definitions can use.
|
|
62
62
|
*/
|
|
63
63
|
commandArgument?: string
|
|
64
|
+
/** The shell the command argument runs in; see {@link ToolDefinition.commandDialect}. */
|
|
65
|
+
commandDialect?: ToolDefinition['commandDialect']
|
|
64
66
|
/** The argument holding a filesystem path; see {@link ToolDefinition.pathArgument}. */
|
|
65
67
|
pathArgument?: string
|
|
68
|
+
/** The argument holding a canonical URL; see {@link ToolDefinition.urlArgument}. */
|
|
69
|
+
urlArgument?: string
|
|
66
70
|
/** The argument asking to leave the sandbox; see {@link ToolDefinition.sandboxEscapeArgument}. */
|
|
67
71
|
sandboxEscapeArgument?: string
|
|
68
72
|
execute(input: z.infer<S>, context: ToolContext): Promise<ToolResult>
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The `isDestructive` functions built from a literal `destructive: true`.
|
|
77
|
+
*
|
|
78
|
+
* Such a tool is destructive for EVERY input, so no call of it can ever be
|
|
79
|
+
* approved without review — and a grant that names it (a skill's
|
|
80
|
+
* `allowed-tools: Write`) would be a promise the review phase never keeps.
|
|
81
|
+
* Kept here rather than as a field on the definition so the public
|
|
82
|
+
* `ToolDefinition` shape does not change; see {@link isAlwaysDestructive}.
|
|
83
|
+
*/
|
|
84
|
+
const ALWAYS_DESTRUCTIVE = new WeakSet<object>()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a tool declares every call destructive, whatever the input.
|
|
88
|
+
*
|
|
89
|
+
* Known only for a tool built by {@link defineTool} with `destructive: true`.
|
|
90
|
+
* A hand-written definition, or one whose flag depends on the input, answers
|
|
91
|
+
* `false`: its calls are still judged one by one, so nothing is lost but an
|
|
92
|
+
* early warning.
|
|
93
|
+
*/
|
|
94
|
+
export function isAlwaysDestructive(tool: Pick<ToolDefinition, 'isDestructive'>): boolean {
|
|
95
|
+
return tool.isDestructive !== undefined && ALWAYS_DESTRUCTIVE.has(tool.isDestructive)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function constantDestructive(value: boolean): () => boolean {
|
|
99
|
+
const fn = () => value
|
|
100
|
+
if (value) ALWAYS_DESTRUCTIVE.add(fn)
|
|
101
|
+
return fn
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
export function defineTool<S extends z.ZodType>(
|
|
72
105
|
options: DefineToolOptions<S>,
|
|
73
106
|
): ToolDefinition<z.infer<S>> {
|
|
@@ -84,7 +117,9 @@ export function defineTool<S extends z.ZodType>(
|
|
|
84
117
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
85
118
|
...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
86
119
|
...(options.commandArgument !== undefined ? { commandArgument: options.commandArgument } : {}),
|
|
120
|
+
...(options.commandDialect !== undefined ? { commandDialect: options.commandDialect } : {}),
|
|
87
121
|
...(options.pathArgument !== undefined ? { pathArgument: options.pathArgument } : {}),
|
|
122
|
+
...(options.urlArgument !== undefined ? { urlArgument: options.urlArgument } : {}),
|
|
88
123
|
...(options.sandboxEscapeArgument !== undefined
|
|
89
124
|
? { sandboxEscapeArgument: options.sandboxEscapeArgument }
|
|
90
125
|
: {}),
|
|
@@ -102,7 +137,7 @@ export function defineTool<S extends z.ZodType>(
|
|
|
102
137
|
isDestructive:
|
|
103
138
|
typeof options.destructive === 'function'
|
|
104
139
|
? options.destructive
|
|
105
|
-
: (
|
|
140
|
+
: constantDestructive(options.destructive as boolean),
|
|
106
141
|
isConcurrencySafe: () => options.concurrencySafe,
|
|
107
142
|
|
|
108
143
|
async execute(input: TInput, context: ToolContext): Promise<ToolResult> {
|
|
@@ -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
|
+
}
|