@namzu/sdk 45.0.0 → 46.0.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 +166 -0
- package/dist/authorization/gate.d.ts +5 -2
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +35 -4
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +56 -6
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +38 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -1
- package/dist/authorization/shell-lexer.js +88 -57
- 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/pricing/catalogue.generated.d.ts.map +1 -1
- package/dist/pricing/catalogue.generated.js +28 -4
- package/dist/pricing/catalogue.generated.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 +6 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +14 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +7 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +15 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +8 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/tool/callable.d.ts +22 -0
- package/dist/registry/tool/callable.d.ts.map +1 -0
- package/dist/registry/tool/callable.js +29 -0
- package/dist/registry/tool/callable.js.map +1 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +8 -1
- package/dist/registry/tool/execute.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/tool-call-admission.d.ts +37 -11
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +38 -12
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +7 -3
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +32 -7
- 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 +43 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +63 -19
- 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/skills/registry.d.ts +6 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +1 -0
- package/dist/skills/registry.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/computer-use-coordinates.d.ts +65 -0
- package/dist/tools/builtins/computer-use-coordinates.d.ts.map +1 -0
- package/dist/tools/builtins/computer-use-coordinates.js +123 -0
- package/dist/tools/builtins/computer-use-coordinates.js.map +1 -0
- package/dist/tools/builtins/computer-use-image.d.ts +77 -0
- package/dist/tools/builtins/computer-use-image.d.ts.map +1 -0
- package/dist/tools/builtins/computer-use-image.js +223 -0
- package/dist/tools/builtins/computer-use-image.js.map +1 -0
- package/dist/tools/builtins/computer-use.d.ts +519 -14
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +1188 -183
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +2 -1
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +1 -1
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +74 -0
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +214 -159
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +8 -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 +71 -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 +17 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
- package/dist/tools/schedules/schedule-tool.js +418 -0
- package/dist/tools/schedules/schedule-tool.js.map +1 -0
- package/dist/tools/schedules/types.d.ts +252 -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 +107 -9
- package/dist/types/authorization/index.d.ts.map +1 -1
- package/dist/types/authorization/index.js +16 -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/computer-use/index.d.ts +176 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/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 +60 -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 +3 -1
- package/src/authorization/gate.ts +37 -4
- package/src/authorization/rules.ts +55 -7
- package/src/authorization/shell-lexer.ts +109 -51
- 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/pricing/catalogue.generated.ts +28 -4
- package/src/pricing/rates.source.json +27 -6
- package/src/prompt/coding-agent-doctrine.ts +1 -0
- package/src/public-runtime.ts +43 -1
- package/src/public-tools.ts +54 -1
- package/src/public-types.ts +55 -0
- package/src/registry/tool/callable.ts +37 -0
- package/src/registry/tool/execute.ts +8 -1
- package/src/runtime/query/declined.ts +12 -0
- package/src/runtime/query/executor/tool-call-admission.ts +60 -16
- package/src/runtime/query/executor.ts +36 -5
- 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 +107 -18
- 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/skills/registry.ts +8 -0
- package/src/tools/builtins/browser-url.ts +251 -0
- package/src/tools/builtins/browser.ts +817 -0
- package/src/tools/builtins/computer-use-coordinates.ts +144 -0
- package/src/tools/builtins/computer-use-image.ts +278 -0
- package/src/tools/builtins/computer-use.ts +1485 -191
- package/src/tools/builtins/index.ts +7 -1
- package/src/tools/builtins/skill.ts +304 -156
- package/src/tools/defineTool.ts +13 -0
- package/src/tools/schedules/index.ts +4 -0
- package/src/tools/schedules/loop-tool.ts +85 -0
- package/src/tools/schedules/present.ts +88 -0
- package/src/tools/schedules/prompt-scan.ts +96 -0
- package/src/tools/schedules/schedule-tool.ts +482 -0
- package/src/tools/schedules/types.ts +265 -0
- package/src/types/authorization/index.ts +81 -2
- package/src/types/browser/index.ts +341 -0
- package/src/types/computer-use/index.ts +202 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +10 -0
- package/src/types/tool/index.ts +61 -0
- package/src/types/tool/presentation.ts +7 -0
- package/src/utils/frontmatter.ts +29 -3
- package/src/utils/id.ts +14 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { GENAI, NAMZU } from '../../../../telemetry/attributes.js'
|
|
2
|
+
import { NamzuError } from '../../../../types/errors/index.js'
|
|
3
|
+
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
4
|
+
import type { ToolCallOutcome } from '../../executor.js'
|
|
5
|
+
import type { IterationContext, PhaseSignal } from './context.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A tool asked for a person: stop before the next model call.
|
|
9
|
+
*
|
|
10
|
+
* Runs after the batch settled, so every result — the one that asked and
|
|
11
|
+
* its siblings — is already in the transcript and queued for the session
|
|
12
|
+
* log. The checkpoint written here is taken from that state, which is what
|
|
13
|
+
* lets a resume continue exactly as it does after a provider pause: the
|
|
14
|
+
* next step is a model call that sees the results.
|
|
15
|
+
*
|
|
16
|
+
* The first request in the batch speaks for it. Two tools that both need a
|
|
17
|
+
* person need the same thing from the operator — to come and look — and
|
|
18
|
+
* one pause answers both.
|
|
19
|
+
*
|
|
20
|
+
* In a delegated child the turn fails with the reason instead. Nobody
|
|
21
|
+
* resumes a child's turn: its parent is waiting on a result, and a failed
|
|
22
|
+
* child with the reason in it is a result the parent's model can act on.
|
|
23
|
+
*/
|
|
24
|
+
export async function* runHandoffPause(
|
|
25
|
+
ctx: IterationContext,
|
|
26
|
+
iterationNum: number,
|
|
27
|
+
results: readonly ToolCallOutcome[],
|
|
28
|
+
): AsyncGenerator<SessionEvent, PhaseSignal> {
|
|
29
|
+
const requested = results.find((result) => result.handoff !== undefined)
|
|
30
|
+
const handoff = requested?.handoff
|
|
31
|
+
if (!requested || !handoff) return 'continue'
|
|
32
|
+
|
|
33
|
+
if (ctx.delegated) {
|
|
34
|
+
ctx.log.info('A tool in a delegated turn needs a person; the turn fails', {
|
|
35
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
36
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
37
|
+
[GENAI.TOOL_NAME]: requested.toolName,
|
|
38
|
+
})
|
|
39
|
+
throw new NamzuError({
|
|
40
|
+
code: 'tool_error',
|
|
41
|
+
message: `${requested.toolName} needs a person: ${handoff.reason}`,
|
|
42
|
+
details: { toolName: requested.toolName, handoff },
|
|
43
|
+
retryable: false,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const checkpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
|
|
48
|
+
await ctx.emitEvent({
|
|
49
|
+
type: 'checkpoint_created',
|
|
50
|
+
turnId: ctx.recorder.turnId,
|
|
51
|
+
checkpointId: checkpoint.id,
|
|
52
|
+
iteration: iterationNum,
|
|
53
|
+
})
|
|
54
|
+
yield* ctx.drainPending()
|
|
55
|
+
|
|
56
|
+
await ctx.emitEvent({
|
|
57
|
+
type: 'turn_paused',
|
|
58
|
+
budget: ctx.recorder.budget?.summary(),
|
|
59
|
+
turnId: ctx.recorder.turnId,
|
|
60
|
+
checkpointId: checkpoint.id,
|
|
61
|
+
reason: handoff.reason,
|
|
62
|
+
handoff,
|
|
63
|
+
})
|
|
64
|
+
yield* ctx.drainPending()
|
|
65
|
+
ctx.recorder.setStopReason('paused')
|
|
66
|
+
ctx.log.info('Turn paused for a person', {
|
|
67
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
68
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
69
|
+
[GENAI.TOOL_NAME]: requested.toolName,
|
|
70
|
+
'namzu.checkpoint.id': checkpoint.id,
|
|
71
|
+
'namzu.runtime.reason': handoff.reason,
|
|
72
|
+
})
|
|
73
|
+
return 'stop'
|
|
74
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ToolCallSummary } from '../../../../types/hitl/index.js'
|
|
|
3
3
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
4
4
|
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
5
5
|
import type { ShellDialect } from '../../../../types/tool/index.js'
|
|
6
|
+
import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
|
|
6
7
|
import type { PreparedToolBatch, ToolCallDenials } from '../../executor.js'
|
|
7
8
|
import {
|
|
8
9
|
awaitProjectInstructionCallback,
|
|
@@ -58,7 +59,9 @@ export async function* runToolReview(
|
|
|
58
59
|
// skill grants to read it the same way. A test double without the method
|
|
59
60
|
// leaves it unset, which reads the line for any POSIX shell.
|
|
60
61
|
const dialectFor = (toolName: string): { commandDialect?: ShellDialect } => {
|
|
61
|
-
const executor = ctx.toolExecutor as {
|
|
62
|
+
const executor = ctx.toolExecutor as {
|
|
63
|
+
commandDialect?: (name: string) => ShellDialect
|
|
64
|
+
}
|
|
62
65
|
return typeof executor.commandDialect === 'function'
|
|
63
66
|
? { commandDialect: executor.commandDialect(toolName) }
|
|
64
67
|
: {}
|
|
@@ -448,7 +451,11 @@ export async function* runToolReview(
|
|
|
448
451
|
}
|
|
449
452
|
for (const path of tc.escalation?.outsidePaths ?? []) {
|
|
450
453
|
await ctx.recorder.recordAudit({
|
|
451
|
-
what: {
|
|
454
|
+
what: {
|
|
455
|
+
action: 'outside_root_access',
|
|
456
|
+
tool: tc.name,
|
|
457
|
+
resource: path,
|
|
458
|
+
},
|
|
452
459
|
outcome: 'approved',
|
|
453
460
|
reason: "the turn's review approved this call",
|
|
454
461
|
})
|
|
@@ -465,7 +472,7 @@ export async function* runToolReview(
|
|
|
465
472
|
})
|
|
466
473
|
yield* ctx.drainPending()
|
|
467
474
|
|
|
468
|
-
const feedback = reviewDecision.feedback ||
|
|
475
|
+
const feedback = reviewDecision.feedback || DECLINED_TOOL_CALL_FEEDBACK
|
|
469
476
|
const denials = new Map(denyAll(feedback))
|
|
470
477
|
await settleEscalations(denials, undefined)
|
|
471
478
|
await settle(denials)
|
|
@@ -495,7 +502,7 @@ export async function* runToolReview(
|
|
|
495
502
|
}
|
|
496
503
|
}
|
|
497
504
|
if (mod.action === 'deny' && !denials.has(mod.toolCallId)) {
|
|
498
|
-
denials.set(mod.toolCallId,
|
|
505
|
+
denials.set(mod.toolCallId, DECLINED_TOOL_CALL_FEEDBACK)
|
|
499
506
|
}
|
|
500
507
|
}
|
|
501
508
|
|
|
@@ -14,6 +14,7 @@ import type { ChatCompletionResponse } from '../../types/provider/index.js'
|
|
|
14
14
|
import type { ToolExecutionSnapshot } from '../../types/session/tool-execution.js'
|
|
15
15
|
import type { Logger } from '../../utils/logger.js'
|
|
16
16
|
import type { RestoredCheckpoint } from './checkpoint.js'
|
|
17
|
+
import { DECLINED_TOOL_CALL_FEEDBACK } from './declined.js'
|
|
17
18
|
import type { PriorToolResults, ToolCallDenials, ToolExecutor } from './executor.js'
|
|
18
19
|
import { PendingAnswers } from './question-park.js'
|
|
19
20
|
import { readToolExecutions } from './tool-executions.js'
|
|
@@ -630,7 +631,7 @@ function derriveDenials(
|
|
|
630
631
|
return new Map()
|
|
631
632
|
|
|
632
633
|
case 'reject_tools': {
|
|
633
|
-
const reason = decision.feedback ||
|
|
634
|
+
const reason = decision.feedback || DECLINED_TOOL_CALL_FEEDBACK
|
|
634
635
|
return new Map(toolCalls.map((tc) => [tc.id, reason]))
|
|
635
636
|
}
|
|
636
637
|
|
|
@@ -642,7 +643,7 @@ function derriveDenials(
|
|
|
642
643
|
const denials = new Map<string, string>()
|
|
643
644
|
for (const mod of decision.modifications) {
|
|
644
645
|
if (mod.action === 'deny') {
|
|
645
|
-
denials.set(mod.toolCallId,
|
|
646
|
+
denials.set(mod.toolCallId, DECLINED_TOOL_CALL_FEEDBACK)
|
|
646
647
|
}
|
|
647
648
|
}
|
|
648
649
|
for (const mod of decision.modifications) {
|
|
@@ -27,6 +27,7 @@ import type { HITLResumeDecision, ResumeHandler, ToolCallSummary } from '../../t
|
|
|
27
27
|
import type { ApprovalPolicy } from '../../types/hitl/policy.js'
|
|
28
28
|
import type { SessionId, TurnId } from '../../types/ids/index.js'
|
|
29
29
|
import { PLAN_MODE_REFUSAL } from '../../types/permission/index.js'
|
|
30
|
+
import { DECLINED_TOOL_CALL_FEEDBACK } from './declined.js'
|
|
30
31
|
|
|
31
32
|
export type ReviewMode =
|
|
32
33
|
/** Ask a person. The default when a `prompt` is supplied. */
|
|
@@ -196,6 +197,28 @@ export const SANDBOX_ESCAPE_UNATTENDED_REFUSAL =
|
|
|
196
197
|
export const OUTSIDE_ROOTS_UNATTENDED_REFUSAL =
|
|
197
198
|
"Refused: a call in this batch reaches a path outside the working directory and the added directories, which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Stay inside the working directory, or tell the user which directory you need so they can add it to the session (the CLI's --add-dir)."
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* What the model is told when a batch would show it the operator's screen for
|
|
202
|
+
* the first time in a session and nobody can be asked.
|
|
203
|
+
*/
|
|
204
|
+
export const SCREEN_CONSENT_UNATTENDED_REFUSAL =
|
|
205
|
+
"Refused: this call would send what is on the user's screen to the model provider, which a person agrees to once per session, and nobody can be asked in this session. Nothing in this batch ran. Tell the user computer use needs their consent in an interactive session."
|
|
206
|
+
|
|
207
|
+
/** What the model is told when the operator declines to share the screen. */
|
|
208
|
+
export const SCREEN_CONSENT_DECLINED_FEEDBACK =
|
|
209
|
+
'The user declined to share their screen in this session. Nothing in this batch ran. Do not take screenshots or read windows again; ask the user how they want to proceed.'
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The sessions whose operator agreed to let the model see the screen.
|
|
213
|
+
*
|
|
214
|
+
* A host keeps one for as long as its sessions live and hands the same box to
|
|
215
|
+
* every policy it builds, so a mode switch keeps the answer and a new session
|
|
216
|
+
* (a different id) is asked again. The policy only adds to it.
|
|
217
|
+
*/
|
|
218
|
+
export interface ScreenConsentRecord {
|
|
219
|
+
readonly sessions: Set<string>
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
/** The batch a person is asked about. */
|
|
200
223
|
export interface ToolReviewRequest {
|
|
201
224
|
/** Originating session, preserved by createReviewHandler for host attribution. */
|
|
@@ -203,6 +226,14 @@ export interface ToolReviewRequest {
|
|
|
203
226
|
/** Originating turn, preserved by createReviewHandler for host attribution. */
|
|
204
227
|
readonly turnId?: TurnId
|
|
205
228
|
readonly toolCalls: readonly ToolCallSummary[]
|
|
229
|
+
/**
|
|
230
|
+
* This batch would send the screen to the model provider for the first
|
|
231
|
+
* time in the session (see {@link ReviewPolicyOptions.screenConsent}).
|
|
232
|
+
* The question is whether the model may see the screen for the rest of
|
|
233
|
+
* the session; a yes also approves this batch, and later screen captures
|
|
234
|
+
* in the session run without asking.
|
|
235
|
+
*/
|
|
236
|
+
readonly screenConsent?: true
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
export type ToolReviewAnswer =
|
|
@@ -249,6 +280,24 @@ export interface ReviewPolicyOptions {
|
|
|
249
280
|
* either way.
|
|
250
281
|
*/
|
|
251
282
|
readonly skillGrants?: 'honour' | 'ignore'
|
|
283
|
+
/**
|
|
284
|
+
* Ask once per session before the model first sees the screen.
|
|
285
|
+
*
|
|
286
|
+
* With a record, a batch holding a call that captures the screen
|
|
287
|
+
* ({@link capturesScreen}) in a session not yet in `sessions` is put to
|
|
288
|
+
* a person first, as a {@link ToolReviewRequest.screenConsent} request,
|
|
289
|
+
* even when every call in it only reads: in `prompt`, `accept-edits` and
|
|
290
|
+
* `plan`. A yes adds the session; a no refuses the batch. `strict`
|
|
291
|
+
* refuses such a call unless a rule allowed it, `auto` never asks, and a
|
|
292
|
+
* policy without a `prompt` refuses. A call a rule allowed is never
|
|
293
|
+
* asked about. Omitted, the screen is treated like any other read.
|
|
294
|
+
*/
|
|
295
|
+
readonly screenConsent?: ScreenConsentRecord
|
|
296
|
+
/**
|
|
297
|
+
* Which calls capture the screen. Default: the tool's own
|
|
298
|
+
* `capturesScreen` declaration, read from `registry`; nothing without one.
|
|
299
|
+
*/
|
|
300
|
+
readonly capturesScreen?: (name: string, input: unknown) => boolean
|
|
252
301
|
}
|
|
253
302
|
|
|
254
303
|
/**
|
|
@@ -275,10 +324,61 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
275
324
|
options.exempt ??
|
|
276
325
|
(registry ? (name, input) => isReviewExempt(registry, name, input) : () => false)
|
|
277
326
|
const remembered = options.remembered ?? { all: false }
|
|
327
|
+
const capturesScreen =
|
|
328
|
+
options.capturesScreen ??
|
|
329
|
+
(registry
|
|
330
|
+
? (name: string, input: unknown) => {
|
|
331
|
+
const tool = registry.get(name) ?? registry.get(name.toLowerCase())
|
|
332
|
+
return tool?.capturesScreen?.(input) === true
|
|
333
|
+
}
|
|
334
|
+
: () => false)
|
|
278
335
|
return async (request): Promise<HITLResumeDecision> => {
|
|
279
336
|
if (request.type !== 'tool_review') {
|
|
280
337
|
return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' }
|
|
281
338
|
}
|
|
339
|
+
// The one answer a person gives for this batch. The screen question
|
|
340
|
+
// below shows the whole batch, so its yes also answers any question
|
|
341
|
+
// the rest of this function would ask; nobody is asked twice.
|
|
342
|
+
let answered: ToolReviewAnswer | undefined
|
|
343
|
+
const ask = async (screenConsent?: true): Promise<ToolReviewAnswer> =>
|
|
344
|
+
answered ??
|
|
345
|
+
(prompt as ToolReviewPrompt)({
|
|
346
|
+
sessionId: request.sessionId,
|
|
347
|
+
turnId: request.turnId,
|
|
348
|
+
toolCalls: request.toolCalls,
|
|
349
|
+
...(screenConsent ? { screenConsent } : {}),
|
|
350
|
+
})
|
|
351
|
+
// The first look at the screen in a session is the operator's to allow:
|
|
352
|
+
// what is on it goes to the model provider, and a screenshot reads as
|
|
353
|
+
// harmlessly as a file read to every rule below. Asked once, in the
|
|
354
|
+
// modes where a person decides; a rule that allowed the call already
|
|
355
|
+
// said yes, and a call a rule denied will not run.
|
|
356
|
+
const consent = options.screenConsent
|
|
357
|
+
const sessionKey = String(request.sessionId ?? '')
|
|
358
|
+
if (
|
|
359
|
+
consent &&
|
|
360
|
+
mode !== 'auto' &&
|
|
361
|
+
!consent.sessions.has(sessionKey) &&
|
|
362
|
+
request.toolCalls.some(
|
|
363
|
+
(tc) =>
|
|
364
|
+
tc.authorization?.decision !== 'allow' &&
|
|
365
|
+
tc.authorization?.decision !== 'deny' &&
|
|
366
|
+
capturesScreen(tc.name, tc.input),
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
if (mode === 'strict') return { action: 'reject_tools', feedback: STRICT_MODE_REFUSAL }
|
|
370
|
+
if (!prompt) return { action: 'reject_tools', feedback: SCREEN_CONSENT_UNATTENDED_REFUSAL }
|
|
371
|
+
const answer = await ask(true)
|
|
372
|
+
if (answer.kind === 'reject') {
|
|
373
|
+
return {
|
|
374
|
+
action: 'reject_tools',
|
|
375
|
+
feedback: answer.feedback ?? SCREEN_CONSENT_DECLINED_FEEDBACK,
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
consent.sessions.add(sessionKey)
|
|
379
|
+
if (answer.kind === 'approve-all') remembered.all = true
|
|
380
|
+
answered = answer
|
|
381
|
+
}
|
|
282
382
|
if (!batchNeedsReview(request.toolCalls, exempt)) {
|
|
283
383
|
return { action: 'approve_tools' }
|
|
284
384
|
}
|
|
@@ -325,15 +425,11 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
325
425
|
? { action: 'approve_tools', confirmedEscalations: escapes }
|
|
326
426
|
: { action: 'reject_tools', feedback: SANDBOX_ESCAPE_UNATTENDED_REFUSAL }
|
|
327
427
|
}
|
|
328
|
-
const answer = await
|
|
329
|
-
sessionId: request.sessionId,
|
|
330
|
-
turnId: request.turnId,
|
|
331
|
-
toolCalls: request.toolCalls,
|
|
332
|
-
})
|
|
428
|
+
const answer = await ask()
|
|
333
429
|
if (answer.kind === 'reject') {
|
|
334
430
|
return {
|
|
335
431
|
action: 'reject_tools',
|
|
336
|
-
feedback: answer.feedback ??
|
|
432
|
+
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
337
433
|
}
|
|
338
434
|
}
|
|
339
435
|
// "Approve all" still latches for the calls that follow; it never
|
|
@@ -347,15 +443,11 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
347
443
|
// With nobody to ask it is refused, not approved.
|
|
348
444
|
if (request.toolCalls.some((tc) => (tc.escalation?.outsidePaths?.length ?? 0) > 0)) {
|
|
349
445
|
if (!prompt) return { action: 'reject_tools', feedback: OUTSIDE_ROOTS_UNATTENDED_REFUSAL }
|
|
350
|
-
const answer = await
|
|
351
|
-
sessionId: request.sessionId,
|
|
352
|
-
turnId: request.turnId,
|
|
353
|
-
toolCalls: request.toolCalls,
|
|
354
|
-
})
|
|
446
|
+
const answer = await ask()
|
|
355
447
|
if (answer.kind === 'reject') {
|
|
356
448
|
return {
|
|
357
449
|
action: 'reject_tools',
|
|
358
|
-
feedback: answer.feedback ??
|
|
450
|
+
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
359
451
|
}
|
|
360
452
|
}
|
|
361
453
|
// Latches for the ordinary calls that follow, never for the next path.
|
|
@@ -382,6 +474,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
382
474
|
),
|
|
383
475
|
)
|
|
384
476
|
if (
|
|
477
|
+
answered === undefined &&
|
|
385
478
|
options.skillGrants !== 'ignore' &&
|
|
386
479
|
needsPerson.length > 0 &&
|
|
387
480
|
needsPerson.every(isSkillGranted)
|
|
@@ -391,11 +484,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
391
484
|
skillGranted: needsPerson.map((tc) => tc.id),
|
|
392
485
|
}
|
|
393
486
|
}
|
|
394
|
-
const answer = await
|
|
395
|
-
sessionId: request.sessionId,
|
|
396
|
-
turnId: request.turnId,
|
|
397
|
-
toolCalls: request.toolCalls,
|
|
398
|
-
})
|
|
487
|
+
const answer = await ask()
|
|
399
488
|
switch (answer.kind) {
|
|
400
489
|
case 'approve':
|
|
401
490
|
return { action: 'approve_tools' }
|
|
@@ -405,7 +494,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
405
494
|
case 'reject':
|
|
406
495
|
return {
|
|
407
496
|
action: 'reject_tools',
|
|
408
|
-
feedback: answer.feedback ??
|
|
497
|
+
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
409
498
|
}
|
|
410
499
|
}
|
|
411
500
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Five-field cron, parsed into sets.
|
|
3
|
+
*
|
|
4
|
+
* Minute, hour, day of month, month, day of week. Lists, ranges, steps and
|
|
5
|
+
* three-letter names; `0` and `7` are both Sunday. Vixie semantics for the
|
|
6
|
+
* two day fields: when BOTH are restricted a day matches if EITHER does, which
|
|
7
|
+
* is what every cron and every existing crontab assumes.
|
|
8
|
+
*
|
|
9
|
+
* Refused, each by name: `L`, `W`, `#`, `?`, a sixth (seconds) field and
|
|
10
|
+
* `@reboot`. A refused token is a better answer than a job that silently
|
|
11
|
+
* means something the operator did not write.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ScheduleValidationError } from './errors.js'
|
|
15
|
+
import type { CronExpression } from './types.js'
|
|
16
|
+
|
|
17
|
+
const MACROS: Readonly<Record<string, string>> = {
|
|
18
|
+
'@yearly': '0 0 1 1 *',
|
|
19
|
+
'@annually': '0 0 1 1 *',
|
|
20
|
+
'@monthly': '0 0 1 * *',
|
|
21
|
+
'@weekly': '0 0 * * 0',
|
|
22
|
+
'@daily': '0 0 * * *',
|
|
23
|
+
'@midnight': '0 0 * * *',
|
|
24
|
+
'@hourly': '0 * * * *',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MONTH_NAMES = [
|
|
28
|
+
'jan',
|
|
29
|
+
'feb',
|
|
30
|
+
'mar',
|
|
31
|
+
'apr',
|
|
32
|
+
'may',
|
|
33
|
+
'jun',
|
|
34
|
+
'jul',
|
|
35
|
+
'aug',
|
|
36
|
+
'sep',
|
|
37
|
+
'oct',
|
|
38
|
+
'nov',
|
|
39
|
+
'dec',
|
|
40
|
+
]
|
|
41
|
+
const DAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
|
42
|
+
|
|
43
|
+
interface FieldSpec {
|
|
44
|
+
readonly label: string
|
|
45
|
+
readonly min: number
|
|
46
|
+
readonly max: number
|
|
47
|
+
readonly names?: readonly string[]
|
|
48
|
+
/** Where `names[0]` sits in the numeric range. */
|
|
49
|
+
readonly nameBase?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const FIELDS: readonly FieldSpec[] = [
|
|
53
|
+
{ label: 'minute', min: 0, max: 59 },
|
|
54
|
+
{ label: 'hour', min: 0, max: 23 },
|
|
55
|
+
{ label: 'day of month', min: 1, max: 31 },
|
|
56
|
+
{ label: 'month', min: 1, max: 12, names: MONTH_NAMES, nameBase: 1 },
|
|
57
|
+
{ label: 'day of week', min: 0, max: 7, names: DAY_NAMES, nameBase: 0 },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
function parseValue(raw: string, field: FieldSpec): number {
|
|
61
|
+
const lower = raw.toLowerCase()
|
|
62
|
+
if (field.names) {
|
|
63
|
+
const at = field.names.indexOf(lower)
|
|
64
|
+
if (at >= 0) return at + (field.nameBase ?? 0)
|
|
65
|
+
}
|
|
66
|
+
if (!/^\d+$/.test(raw)) {
|
|
67
|
+
throw new ScheduleValidationError(`${field.label}: "${raw}" is not a number or a name`, raw)
|
|
68
|
+
}
|
|
69
|
+
const n = Number(raw)
|
|
70
|
+
if (n < field.min || n > field.max) {
|
|
71
|
+
throw new ScheduleValidationError(
|
|
72
|
+
`${field.label}: ${n} is outside ${field.min}-${field.max}`,
|
|
73
|
+
raw,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
return n
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseField(text: string, field: FieldSpec): number[] {
|
|
80
|
+
if (text === '') throw new ScheduleValidationError(`${field.label}: empty field`, text)
|
|
81
|
+
const values = new Set<number>()
|
|
82
|
+
for (const part of text.split(',')) {
|
|
83
|
+
if (part === '') throw new ScheduleValidationError(`${field.label}: empty list entry`, text)
|
|
84
|
+
// Names (`jul`, `wed`) are removed first: they carry the letters the
|
|
85
|
+
// refused `L` and `W` are spelled with.
|
|
86
|
+
const withoutNames = part.replace(/[a-z]{3}/gi, (word) =>
|
|
87
|
+
field.names?.includes(word.toLowerCase()) ? '' : word,
|
|
88
|
+
)
|
|
89
|
+
for (const refused of ['L', 'W', '#', '?']) {
|
|
90
|
+
if (withoutNames.toUpperCase().includes(refused)) {
|
|
91
|
+
throw new ScheduleValidationError(
|
|
92
|
+
`${field.label}: "${refused}" is not supported (in "${part}")`,
|
|
93
|
+
refused,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const [rangeText = '', stepText, extra] = part.split('/')
|
|
98
|
+
if (extra !== undefined) {
|
|
99
|
+
throw new ScheduleValidationError(`${field.label}: "${part}" has more than one step`, part)
|
|
100
|
+
}
|
|
101
|
+
let step = 1
|
|
102
|
+
if (stepText !== undefined) {
|
|
103
|
+
if (!/^\d+$/.test(stepText) || Number(stepText) === 0) {
|
|
104
|
+
throw new ScheduleValidationError(
|
|
105
|
+
`${field.label}: step "${stepText}" must be a whole number above 0`,
|
|
106
|
+
part,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
step = Number(stepText)
|
|
110
|
+
}
|
|
111
|
+
let lo: number
|
|
112
|
+
let hi: number
|
|
113
|
+
if (rangeText === '*') {
|
|
114
|
+
lo = field.min
|
|
115
|
+
hi = field.label === 'day of week' ? 6 : field.max
|
|
116
|
+
} else if (rangeText.includes('-')) {
|
|
117
|
+
const [a = '', b = '', more] = rangeText.split('-')
|
|
118
|
+
if (more !== undefined) {
|
|
119
|
+
throw new ScheduleValidationError(`${field.label}: "${rangeText}" is not a range`, part)
|
|
120
|
+
}
|
|
121
|
+
lo = parseValue(a, field)
|
|
122
|
+
hi = parseValue(b, field)
|
|
123
|
+
if (hi < lo) {
|
|
124
|
+
throw new ScheduleValidationError(
|
|
125
|
+
`${field.label}: range "${rangeText}" runs backwards`,
|
|
126
|
+
rangeText,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
lo = parseValue(rangeText, field)
|
|
131
|
+
// `5/15` means "from 5, every 15", as in Vixie cron.
|
|
132
|
+
hi = stepText !== undefined ? (field.label === 'day of week' ? 6 : field.max) : lo
|
|
133
|
+
}
|
|
134
|
+
for (let v = lo; v <= hi; v += step) values.add(v)
|
|
135
|
+
}
|
|
136
|
+
return [...values].sort((a, b) => a - b)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse a five-field expression or a macro. Throws
|
|
141
|
+
* {@link ScheduleValidationError} naming the offending token.
|
|
142
|
+
*/
|
|
143
|
+
export function parseCronExpression(input: string): CronExpression {
|
|
144
|
+
const trimmed = input.trim()
|
|
145
|
+
if (trimmed === '') throw new ScheduleValidationError('empty cron expression', '')
|
|
146
|
+
let text = trimmed
|
|
147
|
+
if (trimmed.startsWith('@')) {
|
|
148
|
+
const macro = MACROS[trimmed.toLowerCase()]
|
|
149
|
+
if (!macro) {
|
|
150
|
+
throw new ScheduleValidationError(`"${trimmed}" is not a supported macro`, trimmed)
|
|
151
|
+
}
|
|
152
|
+
text = macro
|
|
153
|
+
}
|
|
154
|
+
const fields = text.split(/\s+/)
|
|
155
|
+
if (fields.length === 6) {
|
|
156
|
+
throw new ScheduleValidationError(
|
|
157
|
+
'six fields: a seconds field is not supported; use five fields (minute hour day month weekday)',
|
|
158
|
+
fields[0],
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (fields.length !== 5) {
|
|
162
|
+
throw new ScheduleValidationError(
|
|
163
|
+
`expected five fields (minute hour day month weekday), got ${fields.length}`,
|
|
164
|
+
text,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
const [mi = '', ho = '', dom = '', mon = '', dow = ''] = fields
|
|
168
|
+
const minutes = parseField(mi, FIELDS[0] as FieldSpec)
|
|
169
|
+
const hours = parseField(ho, FIELDS[1] as FieldSpec)
|
|
170
|
+
const daysOfMonth = parseField(dom, FIELDS[2] as FieldSpec)
|
|
171
|
+
const months = parseField(mon, FIELDS[3] as FieldSpec)
|
|
172
|
+
const weekdays = parseField(dow, FIELDS[4] as FieldSpec)
|
|
173
|
+
const normalizedWeekdays = [...new Set(weekdays.map((d) => (d === 7 ? 0 : d)))].sort(
|
|
174
|
+
(a, b) => a - b,
|
|
175
|
+
)
|
|
176
|
+
const fixedTime = !/[*/]/.test(mi) && !/[*/]/.test(ho)
|
|
177
|
+
return {
|
|
178
|
+
source: fields.join(' '),
|
|
179
|
+
minutes,
|
|
180
|
+
hours,
|
|
181
|
+
daysOfMonth,
|
|
182
|
+
months,
|
|
183
|
+
daysOfWeek: normalizedWeekdays,
|
|
184
|
+
domRestricted: !dom.startsWith('*'),
|
|
185
|
+
dowRestricted: !dow.startsWith('*'),
|
|
186
|
+
fixedTime,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Whether the civil date matches the day fields (Vixie OR rule). */
|
|
191
|
+
export function cronMatchesDay(
|
|
192
|
+
expr: CronExpression,
|
|
193
|
+
month: number,
|
|
194
|
+
day: number,
|
|
195
|
+
weekday: number,
|
|
196
|
+
): boolean {
|
|
197
|
+
if (!expr.months.includes(month)) return false
|
|
198
|
+
const domMatch = expr.daysOfMonth.includes(day)
|
|
199
|
+
const dowMatch = expr.daysOfWeek.includes(weekday)
|
|
200
|
+
if (expr.domRestricted && expr.dowRestricted) return domMatch || dowMatch
|
|
201
|
+
return domMatch && dowMatch
|
|
202
|
+
}
|