typeclaw 0.3.1 → 0.5.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/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +88 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +370 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +194 -28
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/channels/types.ts +3 -1
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +400 -67
- package/src/cli/model.ts +14 -4
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/provider.ts +3 -20
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +134 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +48 -4
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +174 -48
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +165 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +519 -12
- package/src/init/oauth-login.ts +17 -3
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +29 -2
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/permissions.ts +24 -7
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +44 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +16 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +144 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +112 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +35 -16
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +70 -7
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +311 -26
package/src/plugin/types.ts
CHANGED
|
@@ -92,7 +92,58 @@ export type PluginExecCronJob = {
|
|
|
92
92
|
timezone?: string
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// In-process handler. Invoked directly by the cron consumer; no shell-out, no
|
|
96
|
+
// WS round-trip, no Bun.spawn. Use this when the cron job needs imperative
|
|
97
|
+
// control flow (probe → maybe prompt → write file) and lives in the same
|
|
98
|
+
// plugin as the logic — there is no need to dress the work up as a CLI
|
|
99
|
+
// command just to schedule it from yourself.
|
|
100
|
+
//
|
|
101
|
+
// `handler` is a TypeScript function reference, so this kind CANNOT appear in
|
|
102
|
+
// cron.json (only plugin-contributed cron registrations can carry it). The
|
|
103
|
+
// schema gate (`parseCronFile` in src/cron/schema.ts) keeps user files limited
|
|
104
|
+
// to `prompt` and `exec`.
|
|
105
|
+
export type PluginHandlerCronJob = {
|
|
106
|
+
schedule: string
|
|
107
|
+
kind: 'handler'
|
|
108
|
+
handler: (ctx: CronHandlerContext) => Promise<void>
|
|
109
|
+
enabled?: boolean
|
|
110
|
+
timezone?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type PluginCronJob = PluginPromptCronJob | PluginExecCronJob | PluginHandlerCronJob
|
|
114
|
+
|
|
115
|
+
// Surface passed into a plugin cron handler. Mirrors the LLM-call capabilities
|
|
116
|
+
// of `ContainerCommandContext` (`prompt`, `subagent`, `exec`) without the
|
|
117
|
+
// CLI-shaped fields (stdin/stdout/stderr, args, exit code) because cron has
|
|
118
|
+
// no caller to pipe to.
|
|
119
|
+
//
|
|
120
|
+
// `signal` is reserved for future cancellation and is currently never aborted
|
|
121
|
+
// by the runtime — the consumer matches the existing prompt/exec cron jobs
|
|
122
|
+
// which also let in-flight work finish on container shutdown. The signal IS
|
|
123
|
+
// already threaded into `ctx.prompt` and `ctx.exec`, so the moment the
|
|
124
|
+
// runtime starts aborting it (e.g. via a future graceful-shutdown path),
|
|
125
|
+
// propagation works without handler-author changes. Handler authors who
|
|
126
|
+
// want to respect future cancellation should still check `ctx.signal.aborted`
|
|
127
|
+
// in long-running loops; nothing fires it today.
|
|
128
|
+
//
|
|
129
|
+
// `origin` is a cron-shaped SessionOrigin so any session the handler spawns
|
|
130
|
+
// via `ctx.prompt` carries the cron job's provenance — same role-inheritance
|
|
131
|
+
// semantics as a `kind: 'exec'` job that shells out to `typeclaw <cmd>`.
|
|
132
|
+
export type CronHandlerContext = {
|
|
133
|
+
readonly jobId: string
|
|
134
|
+
// The plugin that registered this cron job (e.g. 'inbox-watch'). Matches
|
|
135
|
+
// `ContainerCommandContext.name`. Useful for log lines that should be
|
|
136
|
+
// attributable to the plugin, not just the cron id.
|
|
137
|
+
readonly name: string
|
|
138
|
+
readonly agentDir: string
|
|
139
|
+
readonly logger: PluginLogger
|
|
140
|
+
readonly signal: AbortSignal
|
|
141
|
+
readonly permissions: PermissionService
|
|
142
|
+
readonly origin: SessionOrigin
|
|
143
|
+
readonly prompt: (text: string) => Promise<string>
|
|
144
|
+
readonly subagent: (name: string, payload?: unknown) => Promise<void>
|
|
145
|
+
readonly exec: (cmd: TemplateStringsArray, ...values: unknown[]) => Promise<CommandExecResult>
|
|
146
|
+
}
|
|
96
147
|
|
|
97
148
|
export type PluginSkill = {
|
|
98
149
|
description: string
|
|
@@ -267,5 +318,97 @@ export type PluginFixResult = {
|
|
|
267
318
|
export type DefinedPlugin<TConfig = never> = {
|
|
268
319
|
readonly configSchema?: z.ZodType<TConfig>
|
|
269
320
|
readonly permissions?: readonly string[]
|
|
321
|
+
// Permission strings the owner wildcard sentinel MUST NOT auto-expand
|
|
322
|
+
// to. Used by the bundled security plugin to keep audience-leak
|
|
323
|
+
// (high-tier) bypasses off the owner role unless an operator grants
|
|
324
|
+
// them explicitly in roles.owner.permissions[]. Generic by design so
|
|
325
|
+
// any future plugin can carve specific permissions out of the wildcard.
|
|
326
|
+
readonly ownerWildcardExclusions?: readonly string[]
|
|
327
|
+
// Declared by-value (not built inside the factory) so the host-stage CLI
|
|
328
|
+
// can dispatch commands without booting plugin runtime state.
|
|
329
|
+
readonly commands?: Record<string, PluginCommand>
|
|
270
330
|
readonly plugin: (ctx: PluginContext<TConfig>) => Promise<PluginExports>
|
|
271
331
|
}
|
|
332
|
+
|
|
333
|
+
// `surface` controls where a plugin command may run: `'container'` requires
|
|
334
|
+
// the agent runtime (prompt/subagent/exec); `'host'` runs on the user's
|
|
335
|
+
// machine with no agent runtime; `'either'` accepts the intersection ctx
|
|
336
|
+
// and runs on whichever stage the user invoked it from.
|
|
337
|
+
export type PluginCommand = ContainerCommand | HostCommand | EitherCommand
|
|
338
|
+
|
|
339
|
+
export type ContainerCommand<A = unknown> = {
|
|
340
|
+
readonly surface: 'container'
|
|
341
|
+
readonly description: string
|
|
342
|
+
// v1 constraint: `z.object({...})` with primitive (string/number/boolean/
|
|
343
|
+
// literal/enum) leaves so `--help` can render `--<name>=<type>`.
|
|
344
|
+
readonly args?: z.ZodObject<z.ZodRawShape>
|
|
345
|
+
readonly permissions?: readonly string[]
|
|
346
|
+
// When true, runtime spawns a fresh Bun subprocess instead of dispatching
|
|
347
|
+
// in-process. Costs ~150ms cold-start; trade for isolation from the agent.
|
|
348
|
+
readonly isolated?: boolean
|
|
349
|
+
readonly run: (ctx: ContainerCommandContext, args: A) => Promise<number>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export type HostCommand<A = unknown> = {
|
|
353
|
+
readonly surface: 'host'
|
|
354
|
+
readonly description: string
|
|
355
|
+
readonly args?: z.ZodObject<z.ZodRawShape>
|
|
356
|
+
readonly run: (ctx: HostCommandContext, args: A) => Promise<number>
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export type EitherCommand<A = unknown> = {
|
|
360
|
+
readonly surface: 'either'
|
|
361
|
+
readonly description: string
|
|
362
|
+
readonly args?: z.ZodObject<z.ZodRawShape>
|
|
363
|
+
readonly run: (ctx: EitherCommandContext, args: A) => Promise<number>
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type CommandStreams = {
|
|
367
|
+
readonly stdin: ReadableStream<Uint8Array>
|
|
368
|
+
readonly stdout: WritableStream<Uint8Array>
|
|
369
|
+
readonly stderr: WritableStream<Uint8Array>
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export type CommandExecResult = {
|
|
373
|
+
readonly stdout: string
|
|
374
|
+
readonly stderr: string
|
|
375
|
+
readonly exitCode: number
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export type ContainerCommandContext = CommandStreams & {
|
|
379
|
+
// The plugin name (e.g. `'my-utilities'`), NOT the command name. Matches
|
|
380
|
+
// `PluginContext.name`. Use the command's own static name if you need it.
|
|
381
|
+
readonly name: string
|
|
382
|
+
readonly version: string | undefined
|
|
383
|
+
readonly agentDir: string
|
|
384
|
+
readonly logger: PluginLogger
|
|
385
|
+
readonly permissions: PermissionService
|
|
386
|
+
// Caller's origin (cron job, TUI op, parent session). Drives permission
|
|
387
|
+
// resolution inside the command. Dispatcher refuses to run without one.
|
|
388
|
+
readonly origin: SessionOrigin
|
|
389
|
+
readonly signal: AbortSignal
|
|
390
|
+
readonly prompt: (text: string) => Promise<string>
|
|
391
|
+
readonly subagent: (name: string, payload?: unknown) => Promise<void>
|
|
392
|
+
readonly exec: (cmd: TemplateStringsArray, ...values: unknown[]) => Promise<CommandExecResult>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export type HostCommandContext = CommandStreams & {
|
|
396
|
+
// The plugin name, NOT the command name. See `ContainerCommandContext.name`.
|
|
397
|
+
readonly name: string
|
|
398
|
+
readonly version: string | undefined
|
|
399
|
+
// Host path of the agent folder (e.g. the absolute path to the agent
|
|
400
|
+
// folder), NOT `/agent`.
|
|
401
|
+
readonly agentDir: string
|
|
402
|
+
readonly logger: PluginLogger
|
|
403
|
+
readonly signal: AbortSignal
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export type EitherCommandContext = CommandStreams & {
|
|
407
|
+
// The plugin name, NOT the command name. See `ContainerCommandContext.name`.
|
|
408
|
+
readonly name: string
|
|
409
|
+
readonly version: string | undefined
|
|
410
|
+
// Resolves to `/agent` in container, host path on host — same author code.
|
|
411
|
+
readonly agentDir: string
|
|
412
|
+
readonly logger: PluginLogger
|
|
413
|
+
readonly signal: AbortSignal
|
|
414
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type LeafKind = 'string' | 'number' | 'boolean' | 'unknown'
|
|
4
|
+
|
|
5
|
+
export type LeafDescription = {
|
|
6
|
+
kind: LeafKind
|
|
7
|
+
required: boolean
|
|
8
|
+
defaultValue: string | undefined
|
|
9
|
+
description: string | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Walks the chain of Zod 4 wrappers (optional, default, nullable) and returns
|
|
13
|
+
// the inner-most leaf node. Reads `_def.type` (Zod 4's lowercase discriminator)
|
|
14
|
+
// directly because the public `instanceof ZodOptional` checks don't work for
|
|
15
|
+
// `.innerType` — Zod 4 types it as the base `$ZodType`, not the public class
|
|
16
|
+
// hierarchy. If Zod ships a breaking change to `_def.type`, this is the only
|
|
17
|
+
// file that needs to update.
|
|
18
|
+
export function describeLeaf(leaf: unknown): LeafDescription {
|
|
19
|
+
let cur: unknown = leaf
|
|
20
|
+
let required = true
|
|
21
|
+
let defaultValue: string | undefined
|
|
22
|
+
let description: string | undefined
|
|
23
|
+
|
|
24
|
+
while (cur !== null && typeof cur === 'object') {
|
|
25
|
+
const node = cur as {
|
|
26
|
+
_def?: { type?: string; innerType?: unknown; defaultValue?: unknown }
|
|
27
|
+
description?: string
|
|
28
|
+
}
|
|
29
|
+
if (typeof node.description === 'string') description = node.description
|
|
30
|
+
const def = node._def
|
|
31
|
+
if (def === undefined) break
|
|
32
|
+
if (def.type === 'optional') {
|
|
33
|
+
required = false
|
|
34
|
+
cur = def.innerType
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (def.type === 'default') {
|
|
38
|
+
required = false
|
|
39
|
+
const raw = typeof def.defaultValue === 'function' ? (def.defaultValue as () => unknown)() : def.defaultValue
|
|
40
|
+
defaultValue = raw === undefined ? undefined : JSON.stringify(raw)
|
|
41
|
+
cur = def.innerType
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if (def.type === 'nullable') {
|
|
45
|
+
cur = def.innerType
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
return { kind: classify(def.type), required, defaultValue, description }
|
|
49
|
+
}
|
|
50
|
+
return { kind: 'unknown', required, defaultValue, description }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function classify(t: string | undefined): LeafKind {
|
|
54
|
+
switch (t) {
|
|
55
|
+
case 'string':
|
|
56
|
+
case 'literal':
|
|
57
|
+
case 'enum':
|
|
58
|
+
return 'string'
|
|
59
|
+
case 'number':
|
|
60
|
+
case 'int':
|
|
61
|
+
return 'number'
|
|
62
|
+
case 'boolean':
|
|
63
|
+
return 'boolean'
|
|
64
|
+
default:
|
|
65
|
+
return 'unknown'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Coerces a single CLI flag value (string from argv or `true` when bare) to the
|
|
70
|
+
// type the leaf expects. Throws a precise error referencing the flag key when
|
|
71
|
+
// coercion fails; the caller surfaces the message to stderr.
|
|
72
|
+
export function coerceFlag(leaf: unknown, raw: string | true, key: string): unknown {
|
|
73
|
+
const info = describeLeaf(leaf)
|
|
74
|
+
if (info.kind === 'boolean') {
|
|
75
|
+
if (raw === true || raw === 'true') return true
|
|
76
|
+
if (raw === 'false') return false
|
|
77
|
+
throw new Error(`--${key}: expected true/false, got "${raw}"`)
|
|
78
|
+
}
|
|
79
|
+
if (info.kind === 'number') {
|
|
80
|
+
if (raw === true) throw new Error(`--${key} requires a numeric value`)
|
|
81
|
+
if (raw === '') throw new Error(`--${key}: empty value rejected; pass a number`)
|
|
82
|
+
const n = Number(raw)
|
|
83
|
+
if (Number.isNaN(n)) throw new Error(`--${key}: not a number: "${raw}"`)
|
|
84
|
+
return n
|
|
85
|
+
}
|
|
86
|
+
if (raw === true) throw new Error(`--${key} requires a value`)
|
|
87
|
+
return raw
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Returns true when `schema` is a Zod 4 z.object whose leaf properties are all
|
|
91
|
+
// primitive-shaped (string/number/boolean, with optional/default/nullable
|
|
92
|
+
// wrappers). Plugin command args schemas MUST satisfy this in v1.
|
|
93
|
+
export function isPrimitiveZodObject(schema: unknown): schema is z.ZodObject<z.ZodRawShape> {
|
|
94
|
+
if (!(schema instanceof z.ZodObject)) return false
|
|
95
|
+
const shape = (schema as z.ZodObject<z.ZodRawShape>).shape as Record<string, unknown>
|
|
96
|
+
for (const leaf of Object.values(shape)) {
|
|
97
|
+
if (describeLeaf(leaf).kind === 'unknown') return false
|
|
98
|
+
}
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
@@ -21,9 +21,10 @@ export type PartialChannelOrigin = {
|
|
|
21
21
|
authorId: string
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | 'telegram' | 'kakao'> = {
|
|
24
|
+
const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | 'telegram' | 'kakao' | 'github'> = {
|
|
25
25
|
'slack-bot': 'slack',
|
|
26
26
|
'discord-bot': 'discord',
|
|
27
|
+
github: 'github',
|
|
27
28
|
'telegram-bot': 'telegram',
|
|
28
29
|
kakaotalk: 'kakao',
|
|
29
30
|
}
|
package/src/run/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@/agent/subagents'
|
|
13
13
|
import { resolveCapOptionsFromConfig } from '@/bundled-plugins/tool-result-cap'
|
|
14
14
|
import { createChannelManager, createChannelsReloadable, type ChannelManager } from '@/channels'
|
|
15
|
+
import { createTunnelBridge, type TunnelBridge } from '@/channels/tunnel-bridge'
|
|
15
16
|
import { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync } from '@/config'
|
|
16
17
|
import {
|
|
17
18
|
type CronConsumer,
|
|
@@ -26,14 +27,24 @@ import {
|
|
|
26
27
|
} from '@/cron'
|
|
27
28
|
import { CLI_VERSION } from '@/init/cli-version'
|
|
28
29
|
import { loadPlugins, type LoadPluginsResult, pluginCronJobs, type PluginRegistry, summarizeLoaded } from '@/plugin'
|
|
30
|
+
import { createPluginLogger } from '@/plugin/context'
|
|
31
|
+
import type { CronHandlerContext } from '@/plugin/types'
|
|
29
32
|
import { createContainerBroker, publishForwardResult } from '@/portbroker'
|
|
30
33
|
import { ReloadRegistry } from '@/reload'
|
|
31
34
|
import { createClaimController } from '@/role-claim'
|
|
32
35
|
import { hydrateChannelEnvFromSecrets } from '@/secrets'
|
|
33
36
|
import { createServer, type Server } from '@/server'
|
|
37
|
+
import {
|
|
38
|
+
createCommandRunner,
|
|
39
|
+
type CommandRunner,
|
|
40
|
+
type CommandSpawnSubagent,
|
|
41
|
+
runExecForCommand,
|
|
42
|
+
runPromptForCommand,
|
|
43
|
+
} from '@/server/command-runner'
|
|
34
44
|
import { createSessionFactory, type SessionFactory } from '@/sessions'
|
|
35
45
|
import { createStream, type Stream } from '@/stream'
|
|
36
46
|
import { createTui as createTuiDefault, type TuiOptions } from '@/tui'
|
|
47
|
+
import { createTunnelManager, type TunnelManager, type TunnelManagerOptions } from '@/tunnels'
|
|
37
48
|
|
|
38
49
|
import { BUNDLED_PLUGINS } from './bundled-plugins'
|
|
39
50
|
import { buildChannelSessionFactory } from './channel-session-factory'
|
|
@@ -45,6 +56,8 @@ export type TuiFactory = (options: TuiOptions) => { run: () => Promise<void> }
|
|
|
45
56
|
|
|
46
57
|
export type LoadCronFn = (agentDir: string, options?: { subagents?: SubagentRegistry }) => Promise<LoadCronResult>
|
|
47
58
|
export type SchedulerFactory = (options: { cwd: string; file: CronFile; onFire: (job: CronJob) => void }) => Scheduler
|
|
59
|
+
export type ChannelManagerFactory = typeof createChannelManager
|
|
60
|
+
export type TunnelManagerFactory = (options: TunnelManagerOptions) => TunnelManager
|
|
48
61
|
|
|
49
62
|
export type StartAgentOptions = {
|
|
50
63
|
port: number
|
|
@@ -56,6 +69,8 @@ export type StartAgentOptions = {
|
|
|
56
69
|
createSchedulerFor?: SchedulerFactory
|
|
57
70
|
sessionFactory?: SessionFactory
|
|
58
71
|
stream?: Stream
|
|
72
|
+
createChannelManager?: ChannelManagerFactory
|
|
73
|
+
createTunnelManager?: TunnelManagerFactory
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
export type StartAgentResult = {
|
|
@@ -82,6 +97,8 @@ export async function startAgent({
|
|
|
82
97
|
createSchedulerFor,
|
|
83
98
|
sessionFactory = createSessionFactory({ agentDir: cwd }),
|
|
84
99
|
stream = createStream(),
|
|
100
|
+
createChannelManager: createChannelManagerFor = createChannelManager,
|
|
101
|
+
createTunnelManager: createTunnelManagerFor = createTunnelManager,
|
|
85
102
|
}: StartAgentOptions): Promise<StartAgentResult> {
|
|
86
103
|
const reloadRegistry = new ReloadRegistry()
|
|
87
104
|
|
|
@@ -123,6 +140,7 @@ export async function startAgent({
|
|
|
123
140
|
pluginRegistry.cronJobs.length > 0 ||
|
|
124
141
|
pluginRegistry.skills.length > 0 ||
|
|
125
142
|
pluginRegistry.skillsDirs.length > 0 ||
|
|
143
|
+
pluginRegistry.commands.length > 0 ||
|
|
126
144
|
pluginsLoaded.loadedPlugins.length > 0
|
|
127
145
|
|
|
128
146
|
const pluginRuntime = createPluginRuntime({
|
|
@@ -149,10 +167,21 @@ export async function startAgent({
|
|
|
149
167
|
rolesProvider: () => getConfig().roles,
|
|
150
168
|
})
|
|
151
169
|
|
|
152
|
-
const
|
|
170
|
+
const tunnelManager: TunnelManager = createTunnelManagerFor({
|
|
171
|
+
tunnels: getConfig().tunnels,
|
|
172
|
+
stream,
|
|
173
|
+
resolveChannelUpstreamPort: (name) => {
|
|
174
|
+
if (name === 'github') return getConfig().channels.github?.webhookPort ?? null
|
|
175
|
+
return null
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const channelManager = createChannelManagerFor({
|
|
153
180
|
agentDir: cwd,
|
|
154
181
|
channelsConfigRef: () => getConfig().channels,
|
|
155
182
|
aliasesRef: () => getConfig().alias,
|
|
183
|
+
tunnelUrlForChannel: (name) => resolveTunnelUrlForChannel(name, tunnelManager),
|
|
184
|
+
tunnelConfiguredForChannel: (name) => isTunnelConfiguredForChannel(name),
|
|
156
185
|
createSessionForChannel: buildChannelSessionFactory({
|
|
157
186
|
cwd,
|
|
158
187
|
sessionFactory,
|
|
@@ -244,7 +273,48 @@ export async function startAgent({
|
|
|
244
273
|
const cronConsumer = createCronConsumer({
|
|
245
274
|
stream,
|
|
246
275
|
cwd,
|
|
247
|
-
|
|
276
|
+
invokeHandler: async (job) => {
|
|
277
|
+
const snap = pluginRuntime.get()
|
|
278
|
+
const registered = snap.registry.cronJobs.find((j) => j.globalId === job.id)
|
|
279
|
+
const pluginName = registered?.pluginName ?? '<unknown>'
|
|
280
|
+
const logger = createPluginLogger(pluginName)
|
|
281
|
+
const abortController = new AbortController()
|
|
282
|
+
const origin: SessionOrigin = {
|
|
283
|
+
kind: 'cron',
|
|
284
|
+
jobId: job.id,
|
|
285
|
+
jobKind: 'handler',
|
|
286
|
+
...(job.scheduledByRole !== undefined ? { scheduledByRole: job.scheduledByRole } : {}),
|
|
287
|
+
scheduledByOrigin: (job.scheduledByOrigin as SessionOrigin | undefined) ?? { kind: 'config-file' },
|
|
288
|
+
}
|
|
289
|
+
const ctx: CronHandlerContext = {
|
|
290
|
+
jobId: job.id,
|
|
291
|
+
name: pluginName,
|
|
292
|
+
agentDir: cwd,
|
|
293
|
+
logger,
|
|
294
|
+
signal: abortController.signal,
|
|
295
|
+
permissions: pluginsLoaded.permissions,
|
|
296
|
+
origin,
|
|
297
|
+
prompt: (text: string) =>
|
|
298
|
+
runPromptForCommand({
|
|
299
|
+
text,
|
|
300
|
+
origin,
|
|
301
|
+
runtime: pluginRuntime,
|
|
302
|
+
agentDir: cwd,
|
|
303
|
+
permissions: pluginsLoaded.permissions,
|
|
304
|
+
signal: abortController.signal,
|
|
305
|
+
runtimeVersion: runtimeVersionOpt.runtimeVersion,
|
|
306
|
+
containerName: containerNameOpt.containerName,
|
|
307
|
+
}),
|
|
308
|
+
subagent: (subName: string, payload?: unknown) =>
|
|
309
|
+
dispatchSpawnSubagent(subName, payload, {
|
|
310
|
+
spawnedByOrigin: origin,
|
|
311
|
+
}),
|
|
312
|
+
exec: (strings: TemplateStringsArray, ...values: unknown[]) =>
|
|
313
|
+
runExecForCommand(strings, values, { cwd, signal: abortController.signal }),
|
|
314
|
+
}
|
|
315
|
+
await job.handler(ctx)
|
|
316
|
+
},
|
|
317
|
+
createSessionForCron: async (job, refOverride) => {
|
|
248
318
|
const snap = pluginRuntime.get()
|
|
249
319
|
const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
|
|
250
320
|
const sessionId = sessionManager.getSessionId()
|
|
@@ -266,6 +336,7 @@ export async function startAgent({
|
|
|
266
336
|
channelRouter: channelManager.router,
|
|
267
337
|
origin: cronOrigin,
|
|
268
338
|
permissions: pluginsLoaded.permissions,
|
|
339
|
+
...(refOverride !== undefined ? { refOverride } : {}),
|
|
269
340
|
...(snap.hasAnyPluginContent
|
|
270
341
|
? {
|
|
271
342
|
plugins: {
|
|
@@ -310,10 +381,15 @@ export async function startAgent({
|
|
|
310
381
|
)
|
|
311
382
|
}
|
|
312
383
|
|
|
384
|
+
const tunnelBridge: TunnelBridge = createTunnelBridge({ stream, channelManager })
|
|
385
|
+
|
|
313
386
|
reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
|
|
314
387
|
await channelManager.start()
|
|
315
388
|
|
|
316
|
-
|
|
389
|
+
// Captured separately from setSpawnSubagent so both the plugin context and
|
|
390
|
+
// the plugin-command runner can dispatch through the same path. The setter
|
|
391
|
+
// returns void, so without this local binding we couldn't reuse the fn.
|
|
392
|
+
const dispatchSpawnSubagent: CommandSpawnSubagent = async (name, payload, options) => {
|
|
317
393
|
// Resolve the spawning session's role from its origin so the subagent
|
|
318
394
|
// inherits it. Callers (hooks like session.idle) pass the parent origin
|
|
319
395
|
// verbatim; we look up the role rather than letting the caller forge it,
|
|
@@ -333,7 +409,8 @@ export async function startAgent({
|
|
|
333
409
|
...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
|
|
334
410
|
...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
|
|
335
411
|
})
|
|
336
|
-
}
|
|
412
|
+
}
|
|
413
|
+
pluginsLoaded.setSpawnSubagent(dispatchSpawnSubagent)
|
|
337
414
|
pluginsLoaded.markBooted()
|
|
338
415
|
|
|
339
416
|
if (pluginsLoaded.loadedPlugins.length > 0) {
|
|
@@ -365,6 +442,17 @@ export async function startAgent({
|
|
|
365
442
|
: undefined
|
|
366
443
|
const containerBrokerOpt = containerBroker ? { containerBroker } : {}
|
|
367
444
|
|
|
445
|
+
const commandRunnerFactory = (outbound: import('@/server/command-runner').CommandOutbound): CommandRunner =>
|
|
446
|
+
createCommandRunner({
|
|
447
|
+
pluginRuntime,
|
|
448
|
+
permissions: pluginsLoaded.permissions,
|
|
449
|
+
spawnSubagent: dispatchSpawnSubagent,
|
|
450
|
+
agentDir: cwd,
|
|
451
|
+
runtimeVersion: CLI_VERSION,
|
|
452
|
+
containerName,
|
|
453
|
+
outbound,
|
|
454
|
+
})
|
|
455
|
+
|
|
368
456
|
const server = createServer({
|
|
369
457
|
port,
|
|
370
458
|
reloadAll: () => reloadRegistry.reloadAll(),
|
|
@@ -375,12 +463,21 @@ export async function startAgent({
|
|
|
375
463
|
agentDir: cwd,
|
|
376
464
|
pluginRuntime,
|
|
377
465
|
claimController,
|
|
466
|
+
commandRunnerFactory,
|
|
467
|
+
tunnelManager,
|
|
378
468
|
...containerNameOpt,
|
|
379
469
|
...runtimeVersionOpt,
|
|
380
470
|
...tuiTokenOpt,
|
|
381
471
|
...containerBrokerOpt,
|
|
382
472
|
}).start()
|
|
383
473
|
|
|
474
|
+
// Tunnel manager starts AFTER the WS server is up so a slow/hanging
|
|
475
|
+
// provider (PR 2's cloudflared first-URL wait) cannot block TUI, reload,
|
|
476
|
+
// or channel adapter availability. External providers resolve URLs
|
|
477
|
+
// synchronously; future managed providers will resolve asynchronously
|
|
478
|
+
// and broadcast URL events when ready.
|
|
479
|
+
await tunnelManager.start()
|
|
480
|
+
|
|
384
481
|
let stopped = false
|
|
385
482
|
const stop = async () => {
|
|
386
483
|
if (stopped) return
|
|
@@ -390,6 +487,8 @@ export async function startAgent({
|
|
|
390
487
|
subagentConsumer.stop()
|
|
391
488
|
server.stop(true)
|
|
392
489
|
void disposeMaterializedSkills(pluginRuntime)
|
|
490
|
+
tunnelBridge.stop()
|
|
491
|
+
await tunnelManager.stop()
|
|
393
492
|
await channelManager.stop()
|
|
394
493
|
}
|
|
395
494
|
|
|
@@ -436,6 +535,15 @@ function buildLocalTuiUrl(port: number, token: string | null): string {
|
|
|
436
535
|
return url.toString()
|
|
437
536
|
}
|
|
438
537
|
|
|
538
|
+
function resolveTunnelUrlForChannel(channelName: string, tunnelManager: TunnelManager): string | null {
|
|
539
|
+
const tunnel = getConfig().tunnels.find((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
|
|
540
|
+
return tunnel ? tunnelManager.urlFor(tunnel.name) : null
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isTunnelConfiguredForChannel(channelName: string): boolean {
|
|
544
|
+
return getConfig().tunnels.some((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
|
|
545
|
+
}
|
|
546
|
+
|
|
439
547
|
async function disposeMaterializedSkills(pluginRuntime: PluginRuntime): Promise<void> {
|
|
440
548
|
const pending = pluginRuntime.drainPendingDisposal()
|
|
441
549
|
const current = pluginRuntime.get().materializedSkills
|
package/src/secrets/index.ts
CHANGED
package/src/secrets/schema.ts
CHANGED
|
@@ -40,6 +40,23 @@ const telegramBotChannelSchema = z.object({
|
|
|
40
40
|
token: secretFieldSchema.optional(),
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
const githubPatAuthSchema = z.object({
|
|
44
|
+
type: z.literal('pat'),
|
|
45
|
+
token: secretFieldSchema,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const githubAppAuthSchema = z.object({
|
|
49
|
+
type: z.literal('app'),
|
|
50
|
+
appId: z.number().int().positive(),
|
|
51
|
+
privateKey: secretFieldSchema,
|
|
52
|
+
installationId: z.number().int().positive().optional(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const githubChannelSchema = z.object({
|
|
56
|
+
auth: z.discriminatedUnion('type', [githubPatAuthSchema, githubAppAuthSchema]),
|
|
57
|
+
webhookSecret: secretFieldSchema,
|
|
58
|
+
})
|
|
59
|
+
|
|
43
60
|
// Encrypted password envelope produced by src/secrets/encryption.ts. Optional
|
|
44
61
|
// in the schema because legacy v2 accounts (pre-renewal feature) don't have
|
|
45
62
|
// one; the renewal cron treats a missing envelope as "reauth required" and
|
|
@@ -92,6 +109,7 @@ export const channelsSchema = z
|
|
|
92
109
|
.object({
|
|
93
110
|
'slack-bot': slackBotChannelSchema.optional(),
|
|
94
111
|
'discord-bot': discordBotChannelSchema.optional(),
|
|
112
|
+
github: githubChannelSchema.optional(),
|
|
95
113
|
'telegram-bot': telegramBotChannelSchema.optional(),
|
|
96
114
|
kakaotalk: kakaoChannelBlockSchema.optional(),
|
|
97
115
|
})
|
|
@@ -113,6 +131,9 @@ export const secretsFileSchema = z.object({
|
|
|
113
131
|
export type ProviderCredential = z.infer<typeof providerCredentialSchema>
|
|
114
132
|
export type Providers = z.infer<typeof providersSchema>
|
|
115
133
|
export type Channels = z.infer<typeof channelsSchema>
|
|
134
|
+
export type GithubPatAuthBlock = z.infer<typeof githubPatAuthSchema>
|
|
135
|
+
export type GithubAppAuthBlock = z.infer<typeof githubAppAuthSchema>
|
|
136
|
+
export type GithubSecretsBlock = z.infer<typeof githubChannelSchema>
|
|
116
137
|
export type KakaoAccountRecord = z.infer<typeof kakaoAccountRecordSchema>
|
|
117
138
|
export type PendingLoginRecord = z.infer<typeof kakaoPendingLoginRecordSchema>
|
|
118
139
|
export type KakaoChannelBlock = z.infer<typeof kakaoChannelBlockSchema>
|