typeclaw 0.3.0 → 0.4.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 +2 -1
- package/scripts/dump-system-prompt.ts +401 -0
- package/secrets.schema.json +113 -0
- package/src/agent/index.ts +149 -30
- package/src/agent/provider-error.ts +44 -0
- package/src/agent/session-meta.ts +43 -0
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/subagents.ts +8 -0
- package/src/agent/system-prompt.ts +70 -35
- package/src/bundled-plugins/security/index.ts +3 -2
- 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 +286 -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 +28 -2
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- 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 +256 -27
- package/src/cli/model.ts +4 -2
- 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/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/cli/usage.ts +30 -2
- package/src/config/config.ts +90 -4
- package/src/config/reloadable.ts +22 -4
- package/src/container/start.ts +30 -3
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +62 -6
- 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 +59 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/index.ts +505 -9
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +6 -1
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +42 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +138 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +119 -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 +393 -15
- 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-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +5 -4
- 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 +35 -4
- 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/aggregate.ts +30 -1
- package/src/usage/index.ts +3 -2
- package/src/usage/report.ts +103 -3
- package/src/usage/scan.ts +59 -4
- package/typeclaw.schema.json +254 -1
|
@@ -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
|
|
|
@@ -105,7 +122,13 @@ export async function startAgent({
|
|
|
105
122
|
...(cwdConfig.roles !== undefined ? { roles: cwdConfig.roles } : {}),
|
|
106
123
|
})
|
|
107
124
|
|
|
108
|
-
reloadRegistry.register(
|
|
125
|
+
reloadRegistry.register(
|
|
126
|
+
createConfigReloadable({
|
|
127
|
+
cwd,
|
|
128
|
+
permissions: pluginsLoaded.permissions,
|
|
129
|
+
skipMountValidation: containerName !== undefined,
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
109
132
|
const pluginRegistry = pluginsLoaded.registry
|
|
110
133
|
const pluginHooks = pluginsLoaded.hooks
|
|
111
134
|
|
|
@@ -117,6 +140,7 @@ export async function startAgent({
|
|
|
117
140
|
pluginRegistry.cronJobs.length > 0 ||
|
|
118
141
|
pluginRegistry.skills.length > 0 ||
|
|
119
142
|
pluginRegistry.skillsDirs.length > 0 ||
|
|
143
|
+
pluginRegistry.commands.length > 0 ||
|
|
120
144
|
pluginsLoaded.loadedPlugins.length > 0
|
|
121
145
|
|
|
122
146
|
const pluginRuntime = createPluginRuntime({
|
|
@@ -143,10 +167,21 @@ export async function startAgent({
|
|
|
143
167
|
rolesProvider: () => getConfig().roles,
|
|
144
168
|
})
|
|
145
169
|
|
|
146
|
-
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({
|
|
147
180
|
agentDir: cwd,
|
|
148
181
|
channelsConfigRef: () => getConfig().channels,
|
|
149
182
|
aliasesRef: () => getConfig().alias,
|
|
183
|
+
tunnelUrlForChannel: (name) => resolveTunnelUrlForChannel(name, tunnelManager),
|
|
184
|
+
tunnelConfiguredForChannel: (name) => isTunnelConfiguredForChannel(name),
|
|
150
185
|
createSessionForChannel: buildChannelSessionFactory({
|
|
151
186
|
cwd,
|
|
152
187
|
sessionFactory,
|
|
@@ -238,6 +273,47 @@ export async function startAgent({
|
|
|
238
273
|
const cronConsumer = createCronConsumer({
|
|
239
274
|
stream,
|
|
240
275
|
cwd,
|
|
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
|
+
},
|
|
241
317
|
createSessionForCron: async (job) => {
|
|
242
318
|
const snap = pluginRuntime.get()
|
|
243
319
|
const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
|
|
@@ -279,6 +355,7 @@ export async function startAgent({
|
|
|
279
355
|
sessionId,
|
|
280
356
|
agentDir: cwd,
|
|
281
357
|
origin: cronOrigin,
|
|
358
|
+
session,
|
|
282
359
|
...(snap.hasAnyPluginContent ? { hooks: snap.hooks } : {}),
|
|
283
360
|
getTranscriptPath: () => sessionManager.getSessionFile(),
|
|
284
361
|
}
|
|
@@ -303,10 +380,15 @@ export async function startAgent({
|
|
|
303
380
|
)
|
|
304
381
|
}
|
|
305
382
|
|
|
383
|
+
const tunnelBridge: TunnelBridge = createTunnelBridge({ stream, channelManager })
|
|
384
|
+
|
|
306
385
|
reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
|
|
307
386
|
await channelManager.start()
|
|
308
387
|
|
|
309
|
-
|
|
388
|
+
// Captured separately from setSpawnSubagent so both the plugin context and
|
|
389
|
+
// the plugin-command runner can dispatch through the same path. The setter
|
|
390
|
+
// returns void, so without this local binding we couldn't reuse the fn.
|
|
391
|
+
const dispatchSpawnSubagent: CommandSpawnSubagent = async (name, payload, options) => {
|
|
310
392
|
// Resolve the spawning session's role from its origin so the subagent
|
|
311
393
|
// inherits it. Callers (hooks like session.idle) pass the parent origin
|
|
312
394
|
// verbatim; we look up the role rather than letting the caller forge it,
|
|
@@ -321,11 +403,13 @@ export async function startAgent({
|
|
|
321
403
|
agentDir: cwd,
|
|
322
404
|
userPrompt: '',
|
|
323
405
|
payload,
|
|
406
|
+
onProviderError: (message) => console.error(`[subagent] ${name}: LLM call failed: ${message}`),
|
|
324
407
|
...(options?.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
|
|
325
408
|
...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
|
|
326
409
|
...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
|
|
327
410
|
})
|
|
328
|
-
}
|
|
411
|
+
}
|
|
412
|
+
pluginsLoaded.setSpawnSubagent(dispatchSpawnSubagent)
|
|
329
413
|
pluginsLoaded.markBooted()
|
|
330
414
|
|
|
331
415
|
if (pluginsLoaded.loadedPlugins.length > 0) {
|
|
@@ -357,6 +441,17 @@ export async function startAgent({
|
|
|
357
441
|
: undefined
|
|
358
442
|
const containerBrokerOpt = containerBroker ? { containerBroker } : {}
|
|
359
443
|
|
|
444
|
+
const commandRunnerFactory = (outbound: import('@/server/command-runner').CommandOutbound): CommandRunner =>
|
|
445
|
+
createCommandRunner({
|
|
446
|
+
pluginRuntime,
|
|
447
|
+
permissions: pluginsLoaded.permissions,
|
|
448
|
+
spawnSubagent: dispatchSpawnSubagent,
|
|
449
|
+
agentDir: cwd,
|
|
450
|
+
runtimeVersion: CLI_VERSION,
|
|
451
|
+
containerName,
|
|
452
|
+
outbound,
|
|
453
|
+
})
|
|
454
|
+
|
|
360
455
|
const server = createServer({
|
|
361
456
|
port,
|
|
362
457
|
reloadAll: () => reloadRegistry.reloadAll(),
|
|
@@ -367,12 +462,21 @@ export async function startAgent({
|
|
|
367
462
|
agentDir: cwd,
|
|
368
463
|
pluginRuntime,
|
|
369
464
|
claimController,
|
|
465
|
+
commandRunnerFactory,
|
|
466
|
+
tunnelManager,
|
|
370
467
|
...containerNameOpt,
|
|
371
468
|
...runtimeVersionOpt,
|
|
372
469
|
...tuiTokenOpt,
|
|
373
470
|
...containerBrokerOpt,
|
|
374
471
|
}).start()
|
|
375
472
|
|
|
473
|
+
// Tunnel manager starts AFTER the WS server is up so a slow/hanging
|
|
474
|
+
// provider (PR 2's cloudflared first-URL wait) cannot block TUI, reload,
|
|
475
|
+
// or channel adapter availability. External providers resolve URLs
|
|
476
|
+
// synchronously; future managed providers will resolve asynchronously
|
|
477
|
+
// and broadcast URL events when ready.
|
|
478
|
+
await tunnelManager.start()
|
|
479
|
+
|
|
376
480
|
let stopped = false
|
|
377
481
|
const stop = async () => {
|
|
378
482
|
if (stopped) return
|
|
@@ -382,6 +486,8 @@ export async function startAgent({
|
|
|
382
486
|
subagentConsumer.stop()
|
|
383
487
|
server.stop(true)
|
|
384
488
|
void disposeMaterializedSkills(pluginRuntime)
|
|
489
|
+
tunnelBridge.stop()
|
|
490
|
+
await tunnelManager.stop()
|
|
385
491
|
await channelManager.stop()
|
|
386
492
|
}
|
|
387
493
|
|
|
@@ -428,6 +534,15 @@ function buildLocalTuiUrl(port: number, token: string | null): string {
|
|
|
428
534
|
return url.toString()
|
|
429
535
|
}
|
|
430
536
|
|
|
537
|
+
function resolveTunnelUrlForChannel(channelName: string, tunnelManager: TunnelManager): string | null {
|
|
538
|
+
const tunnel = getConfig().tunnels.find((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
|
|
539
|
+
return tunnel ? tunnelManager.urlFor(tunnel.name) : null
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function isTunnelConfiguredForChannel(channelName: string): boolean {
|
|
543
|
+
return getConfig().tunnels.some((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
|
|
544
|
+
}
|
|
545
|
+
|
|
431
546
|
async function disposeMaterializedSkills(pluginRuntime: PluginRuntime): Promise<void> {
|
|
432
547
|
const pending = pluginRuntime.drainPendingDisposal()
|
|
433
548
|
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>
|