typeclaw 0.14.0 → 0.15.1
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/package.json +2 -2
- package/src/agent/system-prompt.ts +10 -9
- package/src/agent/tools/channel-reply.ts +37 -27
- package/src/agent/tools/channel-send.ts +13 -8
- package/src/agent/tools/runtime-notice.ts +28 -0
- package/src/agent/tools/webfetch/tool.ts +1 -0
- package/src/agent/tools/websearch.ts +2 -1
- package/src/channels/adapters/discord-bot.ts +8 -1
- package/src/channels/adapters/kakaotalk-format.ts +239 -0
- package/src/channels/adapters/kakaotalk.ts +54 -5
- package/src/channels/adapters/telegram-bot.ts +11 -1
- package/src/channels/router.ts +204 -21
- package/src/channels/types.ts +22 -0
- package/src/cli/inspect.ts +29 -25
- package/src/config/providers.ts +17 -4
- package/src/container/start.ts +17 -0
- package/src/init/dockerfile.ts +21 -1
- package/src/inspect/live.ts +13 -3
- package/src/sandbox/availability.ts +35 -0
- package/src/sandbox/build.ts +128 -0
- package/src/sandbox/errors.ts +20 -0
- package/src/sandbox/index.ts +14 -0
- package/src/sandbox/policy.ts +47 -0
- package/src/sandbox/quote.ts +18 -0
- package/src/server/index.ts +16 -2
- package/src/shared/index.ts +1 -7
- package/src/shared/local-time.ts +14 -22
- package/src/shared/protocol.ts +4 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +11 -9
- package/typeclaw.schema.json +2 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SandboxPolicyError } from './errors'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SANDBOX_ENV,
|
|
4
|
+
type SandboxCommandFilter,
|
|
5
|
+
type SandboxEnvPolicy,
|
|
6
|
+
type SandboxMount,
|
|
7
|
+
type SandboxPolicy,
|
|
8
|
+
} from './policy'
|
|
9
|
+
import { formatCommand } from './quote'
|
|
10
|
+
|
|
11
|
+
export type SandboxedCommand = {
|
|
12
|
+
argv: string[]
|
|
13
|
+
commandString: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Pure: no I/O, no bwrap availability probe (that is `ensureBwrapAvailable`'s
|
|
17
|
+
// job). Given a bash command and a policy, returns the bwrap-wrapped argv plus
|
|
18
|
+
// a shell-quoted rendering of it. Knows nothing about subagents, origins, or
|
|
19
|
+
// the agent runtime — a consumer resolves a policy from whatever context it
|
|
20
|
+
// has and calls this. Throws SandboxPolicyError only when the consumer opted
|
|
21
|
+
// into the command-filter knobs and the command violates them.
|
|
22
|
+
export function buildSandboxedCommand(command: string, policy: SandboxPolicy = {}): SandboxedCommand {
|
|
23
|
+
if (policy.commandFilter !== undefined) {
|
|
24
|
+
applyCommandFilter(command, policy.commandFilter)
|
|
25
|
+
}
|
|
26
|
+
const argv = buildArgv(command, policy)
|
|
27
|
+
return { argv, commandString: formatCommand(argv) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildArgv(command: string, policy: SandboxPolicy): string[] {
|
|
31
|
+
const bwrap = policy.bwrapPath ?? 'bwrap'
|
|
32
|
+
const argv: string[] = [bwrap, '--unshare-all']
|
|
33
|
+
|
|
34
|
+
if (policy.network === 'inherit') {
|
|
35
|
+
// --unshare-all already unshared the net namespace; --share-net rejoins
|
|
36
|
+
// the outer container's network. Other namespaces (user/pid/mount/ipc/
|
|
37
|
+
// uts/cgroup) stay unshared. Default ('none' / undefined) leaves the net
|
|
38
|
+
// namespace isolated — prompt-injected bash cannot exfiltrate over the
|
|
39
|
+
// network without the consumer explicitly opting in.
|
|
40
|
+
argv.push('--share-net')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const proc = policy.process ?? {}
|
|
44
|
+
if (proc.newSession !== false) {
|
|
45
|
+
// Drops the controlling terminal so the contained process cannot push
|
|
46
|
+
// input back into the agent's tty via TIOCSTI. Mandated by
|
|
47
|
+
// docs/internals/sandbox.mdx. Harmless for a one-shot `bash -c`.
|
|
48
|
+
argv.push('--new-session')
|
|
49
|
+
}
|
|
50
|
+
if (proc.dieWithParent !== false) {
|
|
51
|
+
argv.push('--die-with-parent')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
argv.push('--clearenv')
|
|
55
|
+
for (const [key, value] of Object.entries(resolveEnv(policy.env))) {
|
|
56
|
+
argv.push('--setenv', key, value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
argv.push('--ro-bind', '/usr', '/usr', '--ro-bind', '/etc', '/etc', '--dev', '/dev', '--tmpfs', '/tmp')
|
|
60
|
+
|
|
61
|
+
if ((policy.proc ?? 'tmpfs') === 'tmpfs') {
|
|
62
|
+
// --tmpfs /proc, never --proc /proc (OrbStack's kernel blocks
|
|
63
|
+
// mount("proc",...) from user namespaces) and never --dev-bind /proc /proc
|
|
64
|
+
// (leaks the outer container's /proc/N/environ — including
|
|
65
|
+
// FIREWORKS_API_KEY — into the sandbox). See sandbox.mdx.
|
|
66
|
+
argv.push('--tmpfs', '/proc')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const mount of policy.mounts ?? []) {
|
|
70
|
+
appendMount(argv, mount)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (policy.cwd !== undefined) {
|
|
74
|
+
argv.push('--chdir', policy.cwd)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
argv.push('bash', '-c', command)
|
|
78
|
+
return argv
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendMount(argv: string[], mount: SandboxMount): void {
|
|
82
|
+
switch (mount.type) {
|
|
83
|
+
case 'ro-bind':
|
|
84
|
+
argv.push('--ro-bind', mount.source, mount.dest)
|
|
85
|
+
return
|
|
86
|
+
case 'bind':
|
|
87
|
+
argv.push('--bind', mount.source, mount.dest)
|
|
88
|
+
return
|
|
89
|
+
case 'tmpfs':
|
|
90
|
+
argv.push('--tmpfs', mount.dest)
|
|
91
|
+
return
|
|
92
|
+
case 'dev':
|
|
93
|
+
argv.push('--dev', mount.dest)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveEnv(env: SandboxEnvPolicy | undefined): Record<string, string> {
|
|
99
|
+
const resolved: Record<string, string> = { ...DEFAULT_SANDBOX_ENV, ...env?.set }
|
|
100
|
+
for (const key of env?.passthrough ?? []) {
|
|
101
|
+
const value = process.env[key]
|
|
102
|
+
if (value !== undefined) resolved[key] = value
|
|
103
|
+
}
|
|
104
|
+
return resolved
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Token-boundary match: the normalized command must equal a prefix exactly or
|
|
108
|
+
// start with `prefix + ' '`. Substring matching would let `git-evil ...` slip
|
|
109
|
+
// past a `git` prefix; this does not.
|
|
110
|
+
const ALLOWLIST_WHITESPACE = /\s+/g
|
|
111
|
+
const FORBIDDEN_METACHARS = /[;&|`$()<>\\\n]/
|
|
112
|
+
|
|
113
|
+
function applyCommandFilter(command: string, filter: SandboxCommandFilter): void {
|
|
114
|
+
if (filter.rejectShellMetacharacters === true && FORBIDDEN_METACHARS.test(command)) {
|
|
115
|
+
throw new SandboxPolicyError(
|
|
116
|
+
'command contains a forbidden shell metacharacter. This policy only permits simple commands without ; & | ` $ ( ) < > \\ or newlines.',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
if (filter.allowPrefixes !== undefined) {
|
|
120
|
+
const normalized = command.trim().replace(ALLOWLIST_WHITESPACE, ' ')
|
|
121
|
+
const matched = filter.allowPrefixes.some((p) => normalized === p || normalized.startsWith(`${p} `))
|
|
122
|
+
if (!matched) {
|
|
123
|
+
throw new SandboxPolicyError(
|
|
124
|
+
`command does not match any allowed prefix. Allowed: ${filter.allowPrefixes.join(', ')}`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class SandboxUnavailableError extends Error {
|
|
2
|
+
override readonly name = 'SandboxUnavailableError'
|
|
3
|
+
constructor() {
|
|
4
|
+
super(
|
|
5
|
+
'sandbox unavailable: bwrap binary not found on PATH. Refusing to run a command that requires sandboxing without the kernel boundary in place.',
|
|
6
|
+
)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Raised by the optional command-filter knobs (allowPrefixes,
|
|
11
|
+
// rejectShellMetacharacters). These are consumer-opt-in restrictions layered
|
|
12
|
+
// ABOVE the always-on kernel containment, so a rejection here is a policy
|
|
13
|
+
// decision the consumer asked for — not a failure of the sandbox itself. The
|
|
14
|
+
// message is phrased for the model to read and self-correct from.
|
|
15
|
+
export class SandboxPolicyError extends Error {
|
|
16
|
+
override readonly name = 'SandboxPolicyError'
|
|
17
|
+
constructor(reason: string) {
|
|
18
|
+
super(`sandbox policy rejected command: ${reason}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { buildSandboxedCommand, type SandboxedCommand } from './build'
|
|
2
|
+
export { ensureBwrapAvailable } from './availability'
|
|
3
|
+
export { formatCommand, shellQuote } from './quote'
|
|
4
|
+
export { SandboxPolicyError, SandboxUnavailableError } from './errors'
|
|
5
|
+
export {
|
|
6
|
+
DEFAULT_SANDBOX_ENV,
|
|
7
|
+
type SandboxCommandFilter,
|
|
8
|
+
type SandboxEnvPolicy,
|
|
9
|
+
type SandboxMount,
|
|
10
|
+
type SandboxNetwork,
|
|
11
|
+
type SandboxPolicy,
|
|
12
|
+
type SandboxProcessPolicy,
|
|
13
|
+
type SandboxProcStrategy,
|
|
14
|
+
} from './policy'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type SandboxMount =
|
|
2
|
+
| { type: 'ro-bind'; source: string; dest: string }
|
|
3
|
+
| { type: 'bind'; source: string; dest: string }
|
|
4
|
+
| { type: 'tmpfs'; dest: string }
|
|
5
|
+
| { type: 'dev'; dest: string }
|
|
6
|
+
|
|
7
|
+
export type SandboxNetwork = 'none' | 'inherit'
|
|
8
|
+
|
|
9
|
+
export type SandboxProcStrategy = 'tmpfs' | 'none'
|
|
10
|
+
|
|
11
|
+
export type SandboxEnvPolicy = {
|
|
12
|
+
set?: Record<string, string>
|
|
13
|
+
passthrough?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SandboxCommandFilter = {
|
|
17
|
+
allowPrefixes?: string[]
|
|
18
|
+
rejectShellMetacharacters?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SandboxProcessPolicy = {
|
|
22
|
+
newSession?: boolean
|
|
23
|
+
dieWithParent?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SandboxPolicy = {
|
|
27
|
+
bwrapPath?: string
|
|
28
|
+
cwd?: string
|
|
29
|
+
mounts?: SandboxMount[]
|
|
30
|
+
network?: SandboxNetwork
|
|
31
|
+
env?: SandboxEnvPolicy
|
|
32
|
+
commandFilter?: SandboxCommandFilter
|
|
33
|
+
process?: SandboxProcessPolicy
|
|
34
|
+
proc?: SandboxProcStrategy
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The env the sandbox always re-introduces after `--clearenv`. Anything not
|
|
38
|
+
// listed here (or explicitly named in `env.set` / `env.passthrough` by the
|
|
39
|
+
// consumer) is invisible inside the sandbox. This is the load-bearing leak
|
|
40
|
+
// guard: the container env holds FIREWORKS_API_KEY and GH_TOKEN, and env
|
|
41
|
+
// inheritance is the single highest-risk exfil path for prompt-injected bash.
|
|
42
|
+
// HOME points at /tmp because the sandbox mounts /tmp as a fresh tmpfs.
|
|
43
|
+
export const DEFAULT_SANDBOX_ENV: Record<string, string> = {
|
|
44
|
+
PATH: '/usr/local/bin:/usr/bin:/bin',
|
|
45
|
+
HOME: '/tmp',
|
|
46
|
+
LANG: 'C.UTF-8',
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// POSIX shell quoting for rendering a bwrap argv array into a single
|
|
2
|
+
// `bash -c`-safe string. Today's bash tool accepts a string `command` slot
|
|
3
|
+
// (`mutableArgs.command`), so the sandbox primitive renders its canonical
|
|
4
|
+
// argv into a quoted string the agent runtime can drop in unchanged.
|
|
5
|
+
//
|
|
6
|
+
// This is a local copy of the same helper in `src/update/index.ts`. It is
|
|
7
|
+
// deliberately not promoted to a shared module yet: two call sites do not
|
|
8
|
+
// justify the coupling, and this primitive is meant to stand alone with zero
|
|
9
|
+
// imports from the rest of the tree. Promote to `src/shared/shell.ts` only
|
|
10
|
+
// when a third independent consumer appears.
|
|
11
|
+
export function shellQuote(arg: string): string {
|
|
12
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg
|
|
13
|
+
return `'${arg.replaceAll("'", "'\\''")}'`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatCommand(argv: readonly string[]): string {
|
|
17
|
+
return argv.map(shellQuote).join(' ')
|
|
18
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -1121,7 +1121,9 @@ function handleInspectMessage(
|
|
|
1121
1121
|
|
|
1122
1122
|
if (stream !== undefined && typeof msg.sinceMs === 'number') {
|
|
1123
1123
|
for (const event of stream.scan({ sinceTs: msg.sinceMs, target: { kind: 'broadcast' } })) {
|
|
1124
|
-
|
|
1124
|
+
const payload = broadcastEventToFrame(event)
|
|
1125
|
+
if (!isFrameForWatchedSession(payload, msg.sessionId)) continue
|
|
1126
|
+
sendInspect(ws, { type: 'frame', ts: event.ts, payload })
|
|
1125
1127
|
}
|
|
1126
1128
|
for (const event of stream.scan({ sinceTs: msg.sinceMs, target: { kind: 'cron' } })) {
|
|
1127
1129
|
sendInspect(ws, {
|
|
@@ -1143,7 +1145,9 @@ function handleInspectMessage(
|
|
|
1143
1145
|
|
|
1144
1146
|
if (stream !== undefined) {
|
|
1145
1147
|
ws.data.unsubBroadcast = stream.subscribe({ target: { kind: 'broadcast' } }, (event) => {
|
|
1146
|
-
|
|
1148
|
+
const payload = broadcastEventToFrame(event)
|
|
1149
|
+
if (!isFrameForWatchedSession(payload, msg.sessionId)) return
|
|
1150
|
+
sendInspect(ws, { type: 'frame', ts: event.ts, payload })
|
|
1147
1151
|
})
|
|
1148
1152
|
ws.data.unsubCron = stream.subscribe({ target: { kind: 'cron' } }, (event) => {
|
|
1149
1153
|
sendInspect(ws, {
|
|
@@ -1171,6 +1175,15 @@ function broadcastEventToFrame(event: StreamMessage): InspectFramePayload {
|
|
|
1171
1175
|
}
|
|
1172
1176
|
}
|
|
1173
1177
|
|
|
1178
|
+
// Channel inbounds are published as global broadcasts, so every inspect client
|
|
1179
|
+
// receives every session's inbounds. Drop the ones that don't belong to the
|
|
1180
|
+
// session being watched. Non-inbound broadcasts (subagent completions, cron,
|
|
1181
|
+
// tunnels) stay global — they carry no session identity here.
|
|
1182
|
+
function isFrameForWatchedSession(payload: InspectFramePayload, watchedSessionId: string): boolean {
|
|
1183
|
+
if (payload.kind !== 'channel_inbound') return true
|
|
1184
|
+
return payload.sessionId === watchedSessionId
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1174
1187
|
function readChannelInboundBroadcast(payload: unknown): InspectFramePayload | null {
|
|
1175
1188
|
if (typeof payload !== 'object' || payload === null) return null
|
|
1176
1189
|
const p = payload as Record<string, unknown>
|
|
@@ -1191,6 +1204,7 @@ function readChannelInboundBroadcast(payload: unknown): InspectFramePayload | nu
|
|
|
1191
1204
|
if (decision !== 'engage' && decision !== 'observe' && decision !== 'denied' && decision !== 'claim') return null
|
|
1192
1205
|
return {
|
|
1193
1206
|
kind: 'channel_inbound',
|
|
1207
|
+
...(typeof p.sessionId === 'string' ? { sessionId: p.sessionId } : {}),
|
|
1194
1208
|
adapter: p.adapter,
|
|
1195
1209
|
workspace: p.workspace,
|
|
1196
1210
|
chat: p.chat,
|
package/src/shared/index.ts
CHANGED
|
@@ -24,10 +24,4 @@ export {
|
|
|
24
24
|
type TunnelSnapshot,
|
|
25
25
|
} from './protocol'
|
|
26
26
|
|
|
27
|
-
export {
|
|
28
|
-
formatLocalDate,
|
|
29
|
-
formatLocalDateTime,
|
|
30
|
-
formatLocalWeekday,
|
|
31
|
-
type LocalWeekday,
|
|
32
|
-
resolveLocalTimezoneName,
|
|
33
|
-
} from './local-time'
|
|
27
|
+
export { formatLocalDate, formatLocalDateTime, formatLocalWeekday, resolveLocalTimezoneName } from './local-time'
|
package/src/shared/local-time.ts
CHANGED
|
@@ -37,34 +37,26 @@ export function resolveLocalTimezoneName(): string {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// English
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
40
|
+
// English weekday name for a given Date. The per-turn time anchor renders
|
|
41
|
+
// it so the model has the answer to "what day is it" without computing
|
|
42
|
+
// weekday-from-ISO-date — a step LLMs get wrong often enough to matter.
|
|
43
|
+
// Pre-computing the weekday removes the arithmetic step entirely instead
|
|
44
|
+
// of trusting the model to do it correctly each turn. English only:
|
|
45
|
+
// TypeClaw's users are global, so a single canonical language keeps the
|
|
46
|
+
// anchor compact and lets each agent's SOUL.md decide its reply language.
|
|
47
47
|
//
|
|
48
|
-
// Uses Intl.DateTimeFormat with explicit
|
|
48
|
+
// Uses Intl.DateTimeFormat with an explicit locale. No `timeZone` option:
|
|
49
49
|
// the container's local clock is already host-local (the entrypoint
|
|
50
50
|
// propagates TZ via `-e TZ=<host-tz>`), so the runtime's default zone is
|
|
51
|
-
// the one the user sees.
|
|
52
|
-
//
|
|
53
|
-
//
|
|
51
|
+
// the one the user sees. Falls back to the hand-rolled 7-entry lookup if
|
|
52
|
+
// Intl throws (no-tzdata, locked-down sandbox) — the fallback names stay
|
|
53
|
+
// readable and never make the prefix empty.
|
|
54
54
|
const WEEKDAYS_EN = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const
|
|
55
|
-
const WEEKDAYS_KO = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'] as const
|
|
56
55
|
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
export function formatLocalWeekday(date: Date = new Date()): LocalWeekday {
|
|
60
|
-
const dow = date.getDay()
|
|
61
|
-
const fallback: LocalWeekday = { en: WEEKDAYS_EN[dow]!, ko: WEEKDAYS_KO[dow]! }
|
|
56
|
+
export function formatLocalWeekday(date: Date = new Date()): string {
|
|
62
57
|
try {
|
|
63
|
-
return {
|
|
64
|
-
en: new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date),
|
|
65
|
-
ko: new Intl.DateTimeFormat('ko-KR', { weekday: 'long' }).format(date),
|
|
66
|
-
}
|
|
58
|
+
return new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date)
|
|
67
59
|
} catch {
|
|
68
|
-
return
|
|
60
|
+
return WEEKDAYS_EN[date.getDay()]!
|
|
69
61
|
}
|
|
70
62
|
}
|
package/src/shared/protocol.ts
CHANGED
|
@@ -101,6 +101,10 @@ export type InspectFramePayload =
|
|
|
101
101
|
// text — no batching, no compose-prompt wrapping.
|
|
102
102
|
| {
|
|
103
103
|
kind: 'channel_inbound'
|
|
104
|
+
// Channel session this inbound belongs to. Absent for denied/claim
|
|
105
|
+
// intercepts that fire before a session exists. The inspect server drops
|
|
106
|
+
// frames whose sessionId does not match the watched session.
|
|
107
|
+
sessionId?: string
|
|
104
108
|
adapter: string
|
|
105
109
|
workspace: string
|
|
106
110
|
chat: string
|
|
@@ -11,14 +11,16 @@ This means **you are messaging as a person, not as a bot.** Other participants s
|
|
|
11
11
|
|
|
12
12
|
## What KakaoTalk does NOT support
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
14
|
+
KakaoTalk renders messages as plain text — it has no rich-text formatting. **Write plain text from the start.** The adapter strips common markdown as a safety net before sending (so an accidental `**bold**` won't leak literal asterisks), but treat that as a last-resort guard, not a license to write markdown: the strip removes _markers_, it cannot make formatting-dependent layouts like tables readable. Compose for a plain-text surface and you control the result; lean on the stripper and you get whatever falls out.
|
|
15
|
+
|
|
16
|
+
Specifically, do not rely on any of the following — write the plain-text equivalent yourself:
|
|
17
|
+
|
|
18
|
+
- **Bold / italic / strikethrough** — emphasize with word choice or capitalization (sparingly), not `**asterisks**`.
|
|
19
|
+
- **Headings** — `# H1`, `## H2`, `### H3` carry no visual weight here. Use a short label line or just lead with the point.
|
|
20
|
+
- **Tables** — the stripper cannot rescue a pipe-delimited table; it would collapse into an unreadable line. Use bullet lists or short prose paragraphs instead.
|
|
21
|
+
- **Code fences** — for short snippets, paste the code inline as plain text. For long snippets, summarize and offer to send it via another channel.
|
|
22
|
+
- **Inline code** — just write `foo`, no backticks.
|
|
23
|
+
- **Links with display text** — send the bare URL on its own line; the KakaoTalk client auto-links it. (A `[label](url)` that slips through is reduced to `label (url)`, but a bare URL reads cleaner.)
|
|
22
24
|
- **Mentions** — there is no `@user` syntax that the protocol surfaces. Address people by name in the message body.
|
|
23
25
|
- **Threads / replies-with-quote** — every message is a top-level chat post. There is no per-message reply UI.
|
|
24
26
|
- **Outbound stickers / emoticons** — the KakaoTalk sticker store requires desktop-app purchase flows that the SDK does not replicate. Inbound stickers ARE surfaced (see below), but you cannot send one. If the user asks for a sticker, acknowledge the limit and offer text.
|
|
@@ -106,4 +108,4 @@ The adapter drops every inbound where `event.author_id` equals the logged-in acc
|
|
|
106
108
|
|
|
107
109
|
## When you cannot answer in KakaoTalk
|
|
108
110
|
|
|
109
|
-
If the user asks you to do something the adapter cannot do (
|
|
111
|
+
If the user asks you to do something the adapter cannot do (post in a thread, send a sticker, render a real table), say so plainly. Files are fine — those go through `attachments[]` as described above — but threading, stickers, and rich formatting are real limits. Markdown markers you emit get stripped to plain text automatically, so a stray `**` won't leak; the limit is that nothing renders as formatting, not that it crashes. Acknowledge the limit instead of silently dropping the request.
|
package/typeclaw.schema.json
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"anthropic/claude-haiku-4-5",
|
|
33
33
|
"anthropic/claude-sonnet-4-6",
|
|
34
34
|
"anthropic/claude-opus-4-7",
|
|
35
|
+
"anthropic/claude-opus-4-8",
|
|
35
36
|
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
36
37
|
"zai/glm-4.5-air",
|
|
37
38
|
"zai/glm-4.6",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"anthropic/claude-haiku-4-5",
|
|
60
61
|
"anthropic/claude-sonnet-4-6",
|
|
61
62
|
"anthropic/claude-opus-4-7",
|
|
63
|
+
"anthropic/claude-opus-4-8",
|
|
62
64
|
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
63
65
|
"zai/glm-4.5-air",
|
|
64
66
|
"zai/glm-4.6",
|