typeclaw 0.9.1 → 0.10.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/package.json +2 -2
- package/scripts/require-parallel.ts +41 -15
- package/src/agent/index.ts +9 -7
- package/src/agent/live-subagents.ts +0 -1
- package/src/agent/session-origin.ts +10 -0
- package/src/agent/subagent-completion-reminder.ts +4 -1
- package/src/agent/system-prompt.ts +5 -5
- package/src/agent/tools/restart.ts +13 -2
- package/src/agent/tools/spawn-subagent.ts +0 -1
- package/src/agent/tools/subagent-output.ts +3 -51
- package/src/bundled-plugins/memory/dreaming-state.ts +51 -2
- package/src/bundled-plugins/memory/index.ts +55 -25
- package/src/bundled-plugins/memory/memory-retrieval.ts +1 -1
- package/src/bundled-plugins/memory/migration.ts +21 -17
- package/src/bundled-plugins/memory/stream-io.ts +71 -1
- package/src/bundled-plugins/security/index.ts +19 -17
- package/src/bundled-plugins/security/permissions.ts +9 -8
- package/src/bundled-plugins/security/policies/cron-promotion.ts +26 -9
- package/src/bundled-plugins/security/policies/git-exfil.ts +23 -15
- package/src/bundled-plugins/security/policies/role-promotion.ts +25 -18
- package/src/channels/manager.ts +7 -0
- package/src/channels/router.ts +267 -14
- package/src/channels/schema.ts +22 -1
- package/src/cli/compose.ts +23 -2
- package/src/cli/cron.ts +1 -1
- package/src/cli/inspect.ts +105 -12
- package/src/cli/logs.ts +17 -2
- package/src/cli/role.ts +2 -2
- package/src/compose/logs.ts +8 -4
- package/src/config/config.ts +8 -0
- package/src/config/providers.ts +18 -0
- package/src/container/index.ts +1 -1
- package/src/container/logs.ts +38 -11
- package/src/cron/bridge.ts +25 -4
- package/src/hostd/daemon.ts +44 -24
- package/src/hostd/portbroker-manager.ts +19 -3
- package/src/init/dockerfile.ts +199 -4
- package/src/init/gitignore.ts +8 -0
- package/src/inspect/index.ts +42 -5
- package/src/inspect/live.ts +32 -1
- package/src/inspect/loop.ts +20 -0
- package/src/inspect/render.ts +32 -0
- package/src/inspect/replay.ts +14 -0
- package/src/inspect/types.ts +26 -0
- package/src/permissions/builtins.ts +29 -21
- package/src/permissions/permissions.ts +32 -5
- package/src/role-claim/code.ts +9 -9
- package/src/role-claim/controller.ts +3 -2
- package/src/role-claim/match-rule.ts +14 -19
- package/src/role-claim/pending.ts +2 -2
- package/src/run/index.ts +1 -0
- package/src/server/index.ts +59 -19
- package/src/shared/protocol.ts +30 -0
- package/src/skills/typeclaw-codex-cli/SKILL.md +324 -0
- package/src/skills/typeclaw-codex-cli/references/auth-flow.md +144 -0
- package/src/skills/typeclaw-codex-cli/references/stop-hook.md +92 -0
- package/src/skills/typeclaw-codex-cli/references/tmux-driving.md +239 -0
- package/src/skills/typeclaw-config/SKILL.md +39 -32
- package/src/skills/typeclaw-config/references/recommended-mounts.md +233 -0
- package/src/skills/typeclaw-permissions/SKILL.md +24 -18
- package/src/test-helpers/wait-for.ts +15 -7
- package/typeclaw.schema.json +111 -10
|
@@ -29,21 +29,27 @@ export type BuiltinRoleSpec = {
|
|
|
29
29
|
readonly permissions: readonly string[]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// guards require per-call ack from owner too (the audience-leak rule —
|
|
38
|
-
// owner-in-public-channel must not silently post credentials).
|
|
32
|
+
// Role-to-tier defaults form a strict tower:
|
|
33
|
+
// owner → bypass.low + bypass.medium + bypass.high
|
|
34
|
+
// trusted → bypass.low + bypass.medium
|
|
35
|
+
// member → bypass.low
|
|
36
|
+
// guest → no bypass
|
|
39
37
|
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
38
|
+
// `canBypass` in the bundled security plugin checks the specific tier
|
|
39
|
+
// string for the guard's severity, so each role must carry every tier
|
|
40
|
+
// string at or below its cap (tiers do not cascade implicitly).
|
|
41
|
+
//
|
|
42
|
+
// Owner also carries the wildcard sentinel: the sentinel expands to every
|
|
43
|
+
// plugin-contributed `security.bypass.*` string minus
|
|
44
|
+
// `ownerWildcardExclusions`. The bundled security plugin no longer excludes
|
|
45
|
+
// high-tier strings (owner is meant to bypass them by default under this
|
|
46
|
+
// model), so the sentinel covers per-guard high-tier strings too.
|
|
47
|
+
//
|
|
48
|
+
// Tradeoff: this gives owner audience-leak bypass without per-call ack.
|
|
49
|
+
// The owner-in-public-channel risk is now load-bearing on the operator
|
|
50
|
+
// scoping `roles.owner.match[]` tightly. Default match is TUI-only, where
|
|
51
|
+
// a human is present; configs that widen owner to a channel author should
|
|
52
|
+
// understand they have re-opened audience-leak for that author.
|
|
47
53
|
export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> = {
|
|
48
54
|
owner: {
|
|
49
55
|
match: [{ kind: 'tui' }],
|
|
@@ -57,6 +63,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
|
|
|
57
63
|
CORE_PERMISSIONS.subagentSpawnOperator,
|
|
58
64
|
'security.bypass.low',
|
|
59
65
|
'security.bypass.medium',
|
|
66
|
+
'security.bypass.high',
|
|
60
67
|
OWNER_SECURITY_WILDCARD,
|
|
61
68
|
],
|
|
62
69
|
},
|
|
@@ -70,6 +77,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
|
|
|
70
77
|
CORE_PERMISSIONS.subagentOutput,
|
|
71
78
|
CORE_PERMISSIONS.subagentSpawnOperator,
|
|
72
79
|
'security.bypass.low',
|
|
80
|
+
'security.bypass.medium',
|
|
73
81
|
],
|
|
74
82
|
},
|
|
75
83
|
member: {
|
|
@@ -79,6 +87,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
|
|
|
79
87
|
CORE_PERMISSIONS.subagentSpawn,
|
|
80
88
|
CORE_PERMISSIONS.subagentCancel,
|
|
81
89
|
CORE_PERMISSIONS.subagentOutput,
|
|
90
|
+
'security.bypass.low',
|
|
82
91
|
],
|
|
83
92
|
},
|
|
84
93
|
guest: {
|
|
@@ -88,13 +97,12 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
|
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
// Expands the owner wildcard sentinel against plugin-contributed
|
|
91
|
-
// `security.bypass.*` strings. `wildcardExclusions`
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// flow through the non-sentinel branch).
|
|
100
|
+
// `security.bypass.*` strings. `wildcardExclusions` lets plugins opt
|
|
101
|
+
// specific strings OUT of the wildcard expansion. The bundled security
|
|
102
|
+
// plugin no longer excludes any high-tier strings — owner bypasses every
|
|
103
|
+
// security tier by default under the current role-tower model. The
|
|
104
|
+
// parameter is preserved for third-party plugins that want a different
|
|
105
|
+
// shape (e.g. a future audit-only plugin that never auto-flows to owner).
|
|
98
106
|
export function expandOwnerWildcard(
|
|
99
107
|
ownerPermissions: readonly string[],
|
|
100
108
|
pluginContributed: readonly string[],
|
|
@@ -152,6 +152,29 @@ export function createPermissionService(opts: CreatePermissionServiceOptions = {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// Walk order: owner, trusted, custom roles (in REVERSE declaration order),
|
|
156
|
+
// member, guest. First role whose `match[]` covers the origin wins.
|
|
157
|
+
//
|
|
158
|
+
// Built-in tower: owner > trusted > member > guest. Pinning the tower
|
|
159
|
+
// ahead of any user-declared rule closes a load-bearing footgun in the
|
|
160
|
+
// previous pure-declaration-order resolver: declaring
|
|
161
|
+
// `member.match: ["*"]` before `owner.match: [...]` resolved every
|
|
162
|
+
// channel session — INCLUDING the owner's — to `member`, because the
|
|
163
|
+
// wildcard matched first. The rolePromotion guard then made it
|
|
164
|
+
// un-fixable from inside the demoted session (a member-resolved speaker
|
|
165
|
+
// cannot rewrite `roles` without a TUI-issued ack).
|
|
166
|
+
//
|
|
167
|
+
// Custom roles use REVERSE declaration order: later declarations override
|
|
168
|
+
// earlier ones. This matches the standard "later config wins" mental
|
|
169
|
+
// model — when an operator adds a new role with the same match-scope as
|
|
170
|
+
// an existing one (or appends a new author-pinned override to an existing
|
|
171
|
+
// broad rule), the newer entry takes precedence. The previous "earlier
|
|
172
|
+
// wins" was an arbitrary consequence of map iteration order rather than
|
|
173
|
+
// a deliberate semantic.
|
|
174
|
+
//
|
|
175
|
+
// Custom roles cannot self-promote above trusted (no inherent severity
|
|
176
|
+
// guarantee) and cannot demote themselves below member (declaring a custom
|
|
177
|
+
// role implies the operator wants it to win against bottom catch-alls).
|
|
155
178
|
function buildRoleTable(
|
|
156
179
|
roles: RolesConfig,
|
|
157
180
|
pluginPermissions: readonly string[],
|
|
@@ -160,16 +183,20 @@ function buildRoleTable(
|
|
|
160
183
|
const out: ResolvedRole[] = []
|
|
161
184
|
const seen = new Set<string>()
|
|
162
185
|
|
|
163
|
-
|
|
164
|
-
if (seen.has(name))
|
|
186
|
+
const emit = (name: string): void => {
|
|
187
|
+
if (seen.has(name)) return
|
|
165
188
|
seen.add(name)
|
|
166
189
|
out.push(resolveOne(name, roles[name], pluginPermissions, ownerWildcardExclusions))
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
192
|
+
emit('owner')
|
|
193
|
+
emit('trusted')
|
|
194
|
+
const customRoles = Object.keys(roles).filter((name) => !isBuiltinRoleName(name))
|
|
195
|
+
for (let i = customRoles.length - 1; i >= 0; i--) {
|
|
196
|
+
emit(customRoles[i]!)
|
|
172
197
|
}
|
|
198
|
+
emit('member')
|
|
199
|
+
emit('guest')
|
|
173
200
|
|
|
174
201
|
return out
|
|
175
202
|
}
|
package/src/role-claim/code.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
// Role-claim codes are short, human-typeable tokens the operator sends from
|
|
4
|
-
// their host CLI to the bot
|
|
5
|
-
// channel identity. Shape: `claim-XXXX-YYYY` where each
|
|
6
|
-
// from a Crockford-style base32 alphabet (0-9 + A-Z minus
|
|
7
|
-
// dodge OCR-confusable / profane shapes). 8 chars * 5 bits =
|
|
8
|
-
// entropy, which is overkill for a TTL'd in-memory window but
|
|
9
|
-
// display and dictate over voice.
|
|
4
|
+
// their host CLI to the bot in any chat (DM, group, channel) to prove
|
|
5
|
+
// ownership of that channel identity. Shape: `claim-XXXX-YYYY` where each
|
|
6
|
+
// block is 4 chars from a Crockford-style base32 alphabet (0-9 + A-Z minus
|
|
7
|
+
// I, L, O, U to dodge OCR-confusable / profane shapes). 8 chars * 5 bits =
|
|
8
|
+
// 40 bits of entropy, which is overkill for a TTL'd in-memory window but
|
|
9
|
+
// cheap to display and dictate over voice.
|
|
10
10
|
//
|
|
11
11
|
// The `claim-` prefix lets the channel router recognize potential claim
|
|
12
|
-
// attempts in
|
|
13
|
-
// and distinguishes claim
|
|
14
|
-
// which would otherwise need a regex of its own to disambiguate.
|
|
12
|
+
// attempts in inbound text without scanning the whole body for hex blocks,
|
|
13
|
+
// and distinguishes claim messages from normal first-message text like
|
|
14
|
+
// "hi" which would otherwise need a regex of its own to disambiguate.
|
|
15
15
|
|
|
16
16
|
export const CLAIM_CODE_PREFIX = 'claim-'
|
|
17
17
|
|
|
@@ -10,8 +10,9 @@ import { createPendingClaimRegistry, type PendingClaim, type PendingClaimRegistr
|
|
|
10
10
|
//
|
|
11
11
|
// 1. The host CLI (typeclaw role claim) opens a WS and sends `claim_start`.
|
|
12
12
|
// 2. The WS server forwards that to controller.startClaim().
|
|
13
|
-
// 3. The channel router's claimHandler (also wired here) intercepts
|
|
14
|
-
// bearing the code and calls
|
|
13
|
+
// 3. The channel router's claimHandler (also wired here) intercepts any
|
|
14
|
+
// inbound bearing the code (DM, group, or channel) and calls
|
|
15
|
+
// controller.tryConsumeInbound().
|
|
15
16
|
// 4. On consume, the controller writes to typeclaw.json#roles.<role>.match
|
|
16
17
|
// via grantRole, then reloads the live PermissionService so the new
|
|
17
18
|
// match rule takes effect without a container restart.
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Builds a canonical match-rule DSL string from an inbound channel origin,
|
|
2
|
-
// for the role table. Output
|
|
2
|
+
// for the role table. Output shape is always platform-wide + author:
|
|
3
3
|
//
|
|
4
|
-
// slack
|
|
5
|
-
// discord
|
|
6
|
-
// telegram
|
|
7
|
-
// kakao
|
|
4
|
+
// slack:* author:<authorId>
|
|
5
|
+
// discord:* author:<authorId>
|
|
6
|
+
// telegram:* author:<authorId>
|
|
7
|
+
// kakao:* author:<authorId>
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// "Platform-wide" means every chat the adapter sees on that platform —
|
|
10
|
+
// DMs, group chats, and threads alike — gated by the author qualifier so
|
|
11
|
+
// only this specific human is matched. The intent is: once an operator
|
|
12
|
+
// proves they control a channel identity (by sending a code to the bot),
|
|
13
|
+
// they keep their role wherever they speak from on the same platform. To
|
|
14
|
+
// scope tighter (e.g. one workspace, one chat), the operator edits
|
|
15
|
+
// typeclaw.json by hand; the claim flow is deliberately broad because
|
|
16
|
+
// re-claiming on every new chat would be tedious for the common case.
|
|
13
17
|
|
|
14
18
|
import type { ChannelKey } from '@/channels/types'
|
|
15
19
|
|
|
@@ -31,14 +35,5 @@ const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | '
|
|
|
31
35
|
|
|
32
36
|
export function formatClaimMatchRule(origin: PartialChannelOrigin): string {
|
|
33
37
|
const platform = ADAPTER_TO_PLATFORM[origin.adapter]
|
|
34
|
-
|
|
35
|
-
if (origin.adapter === 'kakaotalk') {
|
|
36
|
-
// Kakao has no workspace; routes use dm/group/open buckets. We can't
|
|
37
|
-
// know which bucket from a partial origin alone (adapter-side classifies
|
|
38
|
-
// it), so claim flows are restricted to DM and we emit the specific
|
|
39
|
-
// chat-id form so the rule grants only this 1:1 conversation, not every
|
|
40
|
-
// DM the agent is in.
|
|
41
|
-
return `${platform}:dm/${origin.chat}${authorQual}`
|
|
42
|
-
}
|
|
43
|
-
return `${platform}:${origin.workspace}${authorQual}`
|
|
38
|
+
return `${platform}:* author:${origin.authorId}`
|
|
44
39
|
}
|
|
@@ -21,8 +21,8 @@ export type PendingClaimRegistry = {
|
|
|
21
21
|
cancel: (code: string) => boolean
|
|
22
22
|
current: () => PendingClaim | null
|
|
23
23
|
// Snapshot of consumption result without actually committing the grant.
|
|
24
|
-
// The router calls this on every
|
|
25
|
-
// when the result is 'consumed'.
|
|
24
|
+
// The router calls this on every claim-code-bearing inbound; the grant
|
|
25
|
+
// only fires when the result is 'consumed'.
|
|
26
26
|
tryConsume: (
|
|
27
27
|
code: string,
|
|
28
28
|
origin: PartialChannelOrigin,
|
package/src/run/index.ts
CHANGED
|
@@ -214,6 +214,7 @@ export async function startAgent({
|
|
|
214
214
|
}),
|
|
215
215
|
permissions: pluginsLoaded.permissions,
|
|
216
216
|
claimHandler: claimController.claimHandler,
|
|
217
|
+
stream,
|
|
217
218
|
})
|
|
218
219
|
|
|
219
220
|
const createSessionForSubagent: import('@/agent/subagents').CreateSessionForSubagent = async (
|
package/src/server/index.ts
CHANGED
|
@@ -1062,15 +1062,7 @@ function handleInspectMessage(
|
|
|
1062
1062
|
|
|
1063
1063
|
if (stream !== undefined && typeof msg.sinceMs === 'number') {
|
|
1064
1064
|
for (const event of stream.scan({ sinceTs: msg.sinceMs, target: { kind: 'broadcast' } })) {
|
|
1065
|
-
sendInspect(ws, {
|
|
1066
|
-
type: 'frame',
|
|
1067
|
-
ts: event.ts,
|
|
1068
|
-
payload: {
|
|
1069
|
-
kind: 'broadcast',
|
|
1070
|
-
payload: event.payload,
|
|
1071
|
-
...(event.meta !== undefined ? { meta: event.meta } : {}),
|
|
1072
|
-
},
|
|
1073
|
-
})
|
|
1065
|
+
sendInspect(ws, { type: 'frame', ts: event.ts, payload: broadcastEventToFrame(event) })
|
|
1074
1066
|
}
|
|
1075
1067
|
for (const event of stream.scan({ sinceTs: msg.sinceMs, target: { kind: 'cron' } })) {
|
|
1076
1068
|
sendInspect(ws, {
|
|
@@ -1092,15 +1084,7 @@ function handleInspectMessage(
|
|
|
1092
1084
|
|
|
1093
1085
|
if (stream !== undefined) {
|
|
1094
1086
|
ws.data.unsubBroadcast = stream.subscribe({ target: { kind: 'broadcast' } }, (event) => {
|
|
1095
|
-
sendInspect(ws, {
|
|
1096
|
-
type: 'frame',
|
|
1097
|
-
ts: event.ts,
|
|
1098
|
-
payload: {
|
|
1099
|
-
kind: 'broadcast',
|
|
1100
|
-
payload: event.payload,
|
|
1101
|
-
...(event.meta !== undefined ? { meta: event.meta } : {}),
|
|
1102
|
-
},
|
|
1103
|
-
})
|
|
1087
|
+
sendInspect(ws, { type: 'frame', ts: event.ts, payload: broadcastEventToFrame(event) })
|
|
1104
1088
|
})
|
|
1105
1089
|
ws.data.unsubCron = stream.subscribe({ target: { kind: 'cron' } }, (event) => {
|
|
1106
1090
|
sendInspect(ws, {
|
|
@@ -1118,6 +1102,52 @@ function extractJobId(target: StreamMessage['target']): string {
|
|
|
1118
1102
|
return target.kind === 'cron' ? target.jobId : ''
|
|
1119
1103
|
}
|
|
1120
1104
|
|
|
1105
|
+
function broadcastEventToFrame(event: StreamMessage): InspectFramePayload {
|
|
1106
|
+
const inbound = readChannelInboundBroadcast(event.payload)
|
|
1107
|
+
if (inbound !== null) return inbound
|
|
1108
|
+
return {
|
|
1109
|
+
kind: 'broadcast',
|
|
1110
|
+
payload: event.payload,
|
|
1111
|
+
...(event.meta !== undefined ? { meta: event.meta } : {}),
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function readChannelInboundBroadcast(payload: unknown): InspectFramePayload | null {
|
|
1116
|
+
if (typeof payload !== 'object' || payload === null) return null
|
|
1117
|
+
const p = payload as Record<string, unknown>
|
|
1118
|
+
if (p.kind !== 'channel-inbound') return null
|
|
1119
|
+
if (typeof p.adapter !== 'string') return null
|
|
1120
|
+
if (typeof p.workspace !== 'string') return null
|
|
1121
|
+
if (typeof p.chat !== 'string') return null
|
|
1122
|
+
if (!(p.thread === null || typeof p.thread === 'string')) return null
|
|
1123
|
+
if (typeof p.authorId !== 'string') return null
|
|
1124
|
+
if (typeof p.authorName !== 'string') return null
|
|
1125
|
+
if (typeof p.authorIsBot !== 'boolean') return null
|
|
1126
|
+
if (typeof p.isDm !== 'boolean') return null
|
|
1127
|
+
if (typeof p.isBotMention !== 'boolean') return null
|
|
1128
|
+
if (typeof p.text !== 'string') return null
|
|
1129
|
+
if (typeof p.externalMessageId !== 'string') return null
|
|
1130
|
+
if (typeof p.ts !== 'number') return null
|
|
1131
|
+
const decision = p.decision
|
|
1132
|
+
if (decision !== 'engage' && decision !== 'observe' && decision !== 'denied' && decision !== 'claim') return null
|
|
1133
|
+
return {
|
|
1134
|
+
kind: 'channel_inbound',
|
|
1135
|
+
adapter: p.adapter,
|
|
1136
|
+
workspace: p.workspace,
|
|
1137
|
+
chat: p.chat,
|
|
1138
|
+
thread: p.thread,
|
|
1139
|
+
authorId: p.authorId,
|
|
1140
|
+
authorName: p.authorName,
|
|
1141
|
+
authorIsBot: p.authorIsBot,
|
|
1142
|
+
isDm: p.isDm,
|
|
1143
|
+
isBotMention: p.isBotMention,
|
|
1144
|
+
text: p.text,
|
|
1145
|
+
externalMessageId: p.externalMessageId,
|
|
1146
|
+
ts: p.ts,
|
|
1147
|
+
decision,
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1121
1151
|
function forwardAgentEventToInspect(
|
|
1122
1152
|
ws: InspectWs,
|
|
1123
1153
|
event: unknown,
|
|
@@ -1128,10 +1158,20 @@ function forwardAgentEventToInspect(
|
|
|
1128
1158
|
const e = event as { type?: unknown }
|
|
1129
1159
|
const now = Date.now()
|
|
1130
1160
|
if (e.type === 'message_update') {
|
|
1131
|
-
const ev = event as { assistantMessageEvent?: { type?: unknown; delta?: unknown } }
|
|
1161
|
+
const ev = event as { assistantMessageEvent?: { type?: unknown; delta?: unknown; content?: unknown } }
|
|
1132
1162
|
const ame = ev.assistantMessageEvent
|
|
1133
1163
|
if (ame?.type === 'text_delta' && typeof ame.delta === 'string') {
|
|
1134
1164
|
sendInspect(ws, { type: 'frame', ts: now, payload: { kind: 'text_delta', sessionId, delta: ame.delta } })
|
|
1165
|
+
return
|
|
1166
|
+
}
|
|
1167
|
+
if (ame?.type === 'thinking_delta' && typeof ame.delta === 'string') {
|
|
1168
|
+
sendInspect(ws, { type: 'frame', ts: now, payload: { kind: 'thinking_delta', sessionId, delta: ame.delta } })
|
|
1169
|
+
return
|
|
1170
|
+
}
|
|
1171
|
+
if (ame?.type === 'thinking_end') {
|
|
1172
|
+
const text = typeof ame.content === 'string' ? ame.content : ''
|
|
1173
|
+
sendInspect(ws, { type: 'frame', ts: now, payload: { kind: 'thinking_end', sessionId, text } })
|
|
1174
|
+
return
|
|
1135
1175
|
}
|
|
1136
1176
|
return
|
|
1137
1177
|
}
|
package/src/shared/protocol.ts
CHANGED
|
@@ -57,6 +57,12 @@ export type InspectClientMessage = {
|
|
|
57
57
|
|
|
58
58
|
export type InspectFramePayload =
|
|
59
59
|
| { kind: 'text_delta'; sessionId: string; delta: string }
|
|
60
|
+
// Reasoning trace from the model, streamed as deltas like text. `thinking_end`
|
|
61
|
+
// closes a thinking block; `text` is the joined deltas (empty when redacted).
|
|
62
|
+
// `redacted: true` means the upstream provider hid the content behind a
|
|
63
|
+
// safety filter and only the opaque continuation payload survives.
|
|
64
|
+
| { kind: 'thinking_delta'; sessionId: string; delta: string }
|
|
65
|
+
| { kind: 'thinking_end'; sessionId: string; text: string; redacted?: boolean }
|
|
60
66
|
| { kind: 'tool_start'; sessionId: string; toolCallId: string; name: string; args: unknown }
|
|
61
67
|
| {
|
|
62
68
|
kind: 'tool_end'
|
|
@@ -87,6 +93,30 @@ export type InspectFramePayload =
|
|
|
87
93
|
}
|
|
88
94
|
| { kind: 'broadcast'; payload: unknown; meta?: Record<string, string> }
|
|
89
95
|
| { kind: 'cron-fire'; jobId: string; payload: unknown }
|
|
96
|
+
// Channel inbound message observed by the router. Surfaced regardless
|
|
97
|
+
// of the engagement decision so inspect can show what the agent saw,
|
|
98
|
+
// not just what it chose to act on. `decision` mirrors the router's
|
|
99
|
+
// EngagementDecision plus 'denied' (channel.respond gate) and 'claim'
|
|
100
|
+
// (role-claim intercept) for completeness. `text` is the raw inbound
|
|
101
|
+
// text — no batching, no compose-prompt wrapping.
|
|
102
|
+
| {
|
|
103
|
+
kind: 'channel_inbound'
|
|
104
|
+
adapter: string
|
|
105
|
+
workspace: string
|
|
106
|
+
chat: string
|
|
107
|
+
thread: string | null
|
|
108
|
+
authorId: string
|
|
109
|
+
authorName: string
|
|
110
|
+
authorIsBot: boolean
|
|
111
|
+
isDm: boolean
|
|
112
|
+
isBotMention: boolean
|
|
113
|
+
text: string
|
|
114
|
+
externalMessageId: string
|
|
115
|
+
// 0 = platform timestamp unknown; the renderer uses the frame's
|
|
116
|
+
// wall-clock ts instead.
|
|
117
|
+
ts: number
|
|
118
|
+
decision: 'engage' | 'observe' | 'denied' | 'claim'
|
|
119
|
+
}
|
|
90
120
|
|
|
91
121
|
export type InspectServerMessage =
|
|
92
122
|
| { type: 'subscribed'; sessionId: string; sessionLive: boolean }
|