@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.7-beta0
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/dist/lib/token-utils.js +2 -2
- package/dist/lib/token-utils.js.map +1 -1
- package/index.ts +141 -0
- package/lib/analysis/tokens.ts +225 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +132 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +125 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +148 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress/index.ts +3 -0
- package/lib/compress/message-utils.ts +250 -0
- package/lib/compress/message.ts +137 -0
- package/lib/compress/pipeline.ts +106 -0
- package/lib/compress/protected-content.ts +154 -0
- package/lib/compress/range-utils.ts +308 -0
- package/lib/compress/range.ts +180 -0
- package/lib/compress/search.ts +267 -0
- package/lib/compress/state.ts +268 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +108 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +1071 -0
- package/lib/hooks.ts +378 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/logger.ts +235 -0
- package/lib/message-ids.ts +172 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +56 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +187 -0
- package/lib/prompts/compress-message.ts +42 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +256 -0
- package/lib/state/state.ts +190 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +112 -0
- package/lib/state/utils.ts +334 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/token-utils.ts +162 -0
- package/lib/ui/notification.ts +346 -0
- package/lib/ui/utils.ts +287 -0
- package/package.json +12 -3
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
package/lib/hooks.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "./state"
|
|
2
|
+
import type { Logger } from "./logger"
|
|
3
|
+
import type { PluginConfig } from "./config"
|
|
4
|
+
import { assignMessageRefs } from "./message-ids"
|
|
5
|
+
import {
|
|
6
|
+
buildPriorityMap,
|
|
7
|
+
buildToolIdList,
|
|
8
|
+
injectCompressNudges,
|
|
9
|
+
injectExtendedSubAgentResults,
|
|
10
|
+
injectMessageIds,
|
|
11
|
+
prune,
|
|
12
|
+
stripHallucinations,
|
|
13
|
+
stripHallucinationsFromString,
|
|
14
|
+
stripStaleMetadata,
|
|
15
|
+
syncCompressionBlocks,
|
|
16
|
+
} from "./messages"
|
|
17
|
+
import { renderSystemPrompt, type PromptStore } from "./prompts"
|
|
18
|
+
import { buildProtectedToolsExtension } from "./prompts/extensions/system"
|
|
19
|
+
import {
|
|
20
|
+
applyPendingCompressionDurations,
|
|
21
|
+
buildCompressionTimingKey,
|
|
22
|
+
consumeCompressionStart,
|
|
23
|
+
resolveCompressionDuration,
|
|
24
|
+
} from "./compress/timing"
|
|
25
|
+
import {
|
|
26
|
+
applyPendingManualTrigger,
|
|
27
|
+
handleContextCommand,
|
|
28
|
+
handleDecompressCommand,
|
|
29
|
+
handleHelpCommand,
|
|
30
|
+
handleManualToggleCommand,
|
|
31
|
+
handleManualTriggerCommand,
|
|
32
|
+
handleRecompressCommand,
|
|
33
|
+
handleStatsCommand,
|
|
34
|
+
handleSweepCommand,
|
|
35
|
+
} from "./commands"
|
|
36
|
+
import { type HostPermissionSnapshot } from "./host-permissions"
|
|
37
|
+
import { compressPermission, syncCompressPermissionState } from "./compress-permission"
|
|
38
|
+
import { checkSession, ensureSessionInitialized, saveSessionState, syncToolCache } from "./state"
|
|
39
|
+
import { cacheSystemPromptTokens } from "./ui/utils"
|
|
40
|
+
|
|
41
|
+
const INTERNAL_AGENT_SIGNATURES = [
|
|
42
|
+
"You are a title generator",
|
|
43
|
+
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
44
|
+
"Summarize what was done in this conversation",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
export function createSystemPromptHandler(
|
|
48
|
+
state: SessionState,
|
|
49
|
+
logger: Logger,
|
|
50
|
+
config: PluginConfig,
|
|
51
|
+
prompts: PromptStore,
|
|
52
|
+
) {
|
|
53
|
+
return async (
|
|
54
|
+
input: { sessionID?: string; model: { limit: { context: number } } },
|
|
55
|
+
output: { system: string[] },
|
|
56
|
+
) => {
|
|
57
|
+
if (input.model?.limit?.context) {
|
|
58
|
+
state.modelContextLimit = input.model.limit.context
|
|
59
|
+
logger.debug("Cached model context limit", { limit: state.modelContextLimit })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const systemText = output.system.join("\n")
|
|
67
|
+
if (INTERNAL_AGENT_SIGNATURES.some((sig) => systemText.includes(sig))) {
|
|
68
|
+
logger.info("Skipping DCP system prompt injection for internal agent")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const effectivePermission =
|
|
73
|
+
input.sessionID && state.sessionId === input.sessionID
|
|
74
|
+
? compressPermission(state, config)
|
|
75
|
+
: config.compress.permission
|
|
76
|
+
|
|
77
|
+
if (effectivePermission === "deny") {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
prompts.reload()
|
|
82
|
+
const runtimePrompts = prompts.getRuntimePrompts()
|
|
83
|
+
const newPrompt = renderSystemPrompt(
|
|
84
|
+
runtimePrompts,
|
|
85
|
+
buildProtectedToolsExtension(config.compress.protectedTools),
|
|
86
|
+
!!state.manualMode,
|
|
87
|
+
state.isSubAgent && config.experimental.allowSubAgents,
|
|
88
|
+
)
|
|
89
|
+
if (output.system.length > 0) {
|
|
90
|
+
output.system[output.system.length - 1] += "\n\n" + newPrompt
|
|
91
|
+
} else {
|
|
92
|
+
output.system.push(newPrompt)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createChatMessageTransformHandler(
|
|
98
|
+
client: any,
|
|
99
|
+
state: SessionState,
|
|
100
|
+
logger: Logger,
|
|
101
|
+
config: PluginConfig,
|
|
102
|
+
prompts: PromptStore,
|
|
103
|
+
hostPermissions: HostPermissionSnapshot,
|
|
104
|
+
) {
|
|
105
|
+
return async (input: {}, output: { messages: WithParts[] }) => {
|
|
106
|
+
await checkSession(client, state, logger, output.messages, config.manualMode.enabled)
|
|
107
|
+
|
|
108
|
+
syncCompressPermissionState(state, config, hostPermissions, output.messages)
|
|
109
|
+
|
|
110
|
+
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
stripHallucinations(output.messages)
|
|
115
|
+
cacheSystemPromptTokens(state, output.messages)
|
|
116
|
+
assignMessageRefs(state, output.messages)
|
|
117
|
+
syncCompressionBlocks(state, logger, output.messages)
|
|
118
|
+
syncToolCache(state, config, logger, output.messages)
|
|
119
|
+
buildToolIdList(state, output.messages)
|
|
120
|
+
prune(state, logger, config, output.messages)
|
|
121
|
+
await injectExtendedSubAgentResults(
|
|
122
|
+
client,
|
|
123
|
+
state,
|
|
124
|
+
logger,
|
|
125
|
+
output.messages,
|
|
126
|
+
config.experimental.allowSubAgents,
|
|
127
|
+
)
|
|
128
|
+
const compressionPriorities = buildPriorityMap(config, state, output.messages)
|
|
129
|
+
prompts.reload()
|
|
130
|
+
injectCompressNudges(
|
|
131
|
+
state,
|
|
132
|
+
config,
|
|
133
|
+
logger,
|
|
134
|
+
output.messages,
|
|
135
|
+
prompts.getRuntimePrompts(),
|
|
136
|
+
compressionPriorities,
|
|
137
|
+
)
|
|
138
|
+
injectMessageIds(state, config, output.messages, compressionPriorities)
|
|
139
|
+
applyPendingManualTrigger(state, output.messages, logger)
|
|
140
|
+
stripStaleMetadata(output.messages)
|
|
141
|
+
|
|
142
|
+
if (state.sessionId) {
|
|
143
|
+
await logger.saveContext(state.sessionId, output.messages)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createCommandExecuteHandler(
|
|
149
|
+
client: any,
|
|
150
|
+
state: SessionState,
|
|
151
|
+
logger: Logger,
|
|
152
|
+
config: PluginConfig,
|
|
153
|
+
workingDirectory: string,
|
|
154
|
+
hostPermissions: HostPermissionSnapshot,
|
|
155
|
+
) {
|
|
156
|
+
return async (
|
|
157
|
+
input: { command: string; sessionID: string; arguments: string },
|
|
158
|
+
output: { parts: any[] },
|
|
159
|
+
) => {
|
|
160
|
+
if (!config.commands.enabled) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (input.command === "dcp") {
|
|
165
|
+
const messagesResponse = await client.session.messages({
|
|
166
|
+
path: { id: input.sessionID },
|
|
167
|
+
})
|
|
168
|
+
const messages = (messagesResponse.data || messagesResponse) as WithParts[]
|
|
169
|
+
|
|
170
|
+
await ensureSessionInitialized(
|
|
171
|
+
client,
|
|
172
|
+
state,
|
|
173
|
+
input.sessionID,
|
|
174
|
+
logger,
|
|
175
|
+
messages,
|
|
176
|
+
config.manualMode.enabled,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
syncCompressPermissionState(state, config, hostPermissions, messages)
|
|
180
|
+
|
|
181
|
+
const effectivePermission = compressPermission(state, config)
|
|
182
|
+
if (effectivePermission === "deny") {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
|
|
187
|
+
const subcommand = args[0]?.toLowerCase() || ""
|
|
188
|
+
const subArgs = args.slice(1)
|
|
189
|
+
|
|
190
|
+
const commandCtx = {
|
|
191
|
+
client,
|
|
192
|
+
state,
|
|
193
|
+
config,
|
|
194
|
+
logger,
|
|
195
|
+
sessionId: input.sessionID,
|
|
196
|
+
messages,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (subcommand === "context") {
|
|
200
|
+
await handleContextCommand(commandCtx)
|
|
201
|
+
throw new Error("__DCP_CONTEXT_HANDLED__")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (subcommand === "stats") {
|
|
205
|
+
await handleStatsCommand(commandCtx)
|
|
206
|
+
throw new Error("__DCP_STATS_HANDLED__")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (subcommand === "sweep") {
|
|
210
|
+
await handleSweepCommand({
|
|
211
|
+
...commandCtx,
|
|
212
|
+
args: subArgs,
|
|
213
|
+
workingDirectory,
|
|
214
|
+
})
|
|
215
|
+
throw new Error("__DCP_SWEEP_HANDLED__")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (subcommand === "manual") {
|
|
219
|
+
await handleManualToggleCommand(commandCtx, subArgs[0]?.toLowerCase())
|
|
220
|
+
throw new Error("__DCP_MANUAL_HANDLED__")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (subcommand === "compress") {
|
|
224
|
+
const userFocus = subArgs.join(" ").trim()
|
|
225
|
+
const prompt = await handleManualTriggerCommand(commandCtx, "compress", userFocus)
|
|
226
|
+
if (!prompt) {
|
|
227
|
+
throw new Error("__DCP_MANUAL_TRIGGER_BLOCKED__")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
state.manualMode = "compress-pending"
|
|
231
|
+
state.pendingManualTrigger = {
|
|
232
|
+
sessionId: input.sessionID,
|
|
233
|
+
prompt,
|
|
234
|
+
}
|
|
235
|
+
const rawArgs = (input.arguments || "").trim()
|
|
236
|
+
output.parts.length = 0
|
|
237
|
+
output.parts.push({
|
|
238
|
+
type: "text",
|
|
239
|
+
text: rawArgs ? `/dcp ${rawArgs}` : `/dcp ${subcommand}`,
|
|
240
|
+
})
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (subcommand === "decompress") {
|
|
245
|
+
await handleDecompressCommand({
|
|
246
|
+
...commandCtx,
|
|
247
|
+
args: subArgs,
|
|
248
|
+
})
|
|
249
|
+
throw new Error("__DCP_DECOMPRESS_HANDLED__")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (subcommand === "recompress") {
|
|
253
|
+
await handleRecompressCommand({
|
|
254
|
+
...commandCtx,
|
|
255
|
+
args: subArgs,
|
|
256
|
+
})
|
|
257
|
+
throw new Error("__DCP_RECOMPRESS_HANDLED__")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await handleHelpCommand(commandCtx)
|
|
261
|
+
throw new Error("__DCP_HELP_HANDLED__")
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function createTextCompleteHandler() {
|
|
267
|
+
return async (
|
|
268
|
+
_input: { sessionID: string; messageID: string; partID: string },
|
|
269
|
+
output: { text: string },
|
|
270
|
+
) => {
|
|
271
|
+
output.text = stripHallucinationsFromString(output.text)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function createEventHandler(state: SessionState, logger: Logger) {
|
|
276
|
+
return async (input: { event: any }) => {
|
|
277
|
+
const eventTime =
|
|
278
|
+
typeof input.event?.time === "number" && Number.isFinite(input.event.time)
|
|
279
|
+
? input.event.time
|
|
280
|
+
: typeof input.event?.properties?.time === "number" &&
|
|
281
|
+
Number.isFinite(input.event.properties.time)
|
|
282
|
+
? input.event.properties.time
|
|
283
|
+
: undefined
|
|
284
|
+
|
|
285
|
+
if (input.event.type !== "message.part.updated") {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const part = input.event.properties?.part
|
|
290
|
+
if (part?.type !== "tool" || part.tool !== "compress") {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (part.state.status === "pending") {
|
|
295
|
+
if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const startedAt = eventTime ?? Date.now()
|
|
300
|
+
const key = buildCompressionTimingKey(part.messageID, part.callID)
|
|
301
|
+
if (state.compressionTiming.startsByCallId.has(key)) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
state.compressionTiming.startsByCallId.set(key, startedAt)
|
|
305
|
+
logger.debug("Recorded compression start", {
|
|
306
|
+
messageID: part.messageID,
|
|
307
|
+
callID: part.callID,
|
|
308
|
+
startedAt,
|
|
309
|
+
})
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (part.state.status === "completed") {
|
|
314
|
+
if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const key = buildCompressionTimingKey(part.messageID, part.callID)
|
|
319
|
+
const start = consumeCompressionStart(state, part.messageID, part.callID)
|
|
320
|
+
const durationMs = resolveCompressionDuration(start, eventTime, part.state.time)
|
|
321
|
+
if (typeof durationMs !== "number") {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
state.compressionTiming.pendingByCallId.set(key, {
|
|
326
|
+
messageId: part.messageID,
|
|
327
|
+
callId: part.callID,
|
|
328
|
+
durationMs,
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const updates = applyPendingCompressionDurations(state)
|
|
332
|
+
if (updates === 0) {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await saveSessionState(state, logger)
|
|
337
|
+
|
|
338
|
+
logger.info("Attached compression time to blocks", {
|
|
339
|
+
messageID: part.messageID,
|
|
340
|
+
callID: part.callID,
|
|
341
|
+
blocks: updates,
|
|
342
|
+
durationMs,
|
|
343
|
+
})
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (part.state.status === "running") {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (typeof part.callID === "string" && typeof part.messageID === "string") {
|
|
352
|
+
state.compressionTiming.startsByCallId.delete(
|
|
353
|
+
buildCompressionTimingKey(part.messageID, part.callID),
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function createChatMessageHandler(
|
|
360
|
+
state: SessionState,
|
|
361
|
+
logger: Logger,
|
|
362
|
+
_config: PluginConfig,
|
|
363
|
+
_hostPermissions: HostPermissionSnapshot,
|
|
364
|
+
) {
|
|
365
|
+
return async (
|
|
366
|
+
input: {
|
|
367
|
+
sessionID: string
|
|
368
|
+
agent?: string
|
|
369
|
+
model?: { providerID: string; modelID: string }
|
|
370
|
+
messageID?: string
|
|
371
|
+
variant?: string
|
|
372
|
+
},
|
|
373
|
+
_output: any,
|
|
374
|
+
) => {
|
|
375
|
+
state.variant = input.variant
|
|
376
|
+
logger.debug("Cached variant from chat.message hook", { variant: input.variant })
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type PermissionAction = "ask" | "allow" | "deny"
|
|
2
|
+
|
|
3
|
+
export type PermissionValue = PermissionAction | Record<string, PermissionAction>
|
|
4
|
+
|
|
5
|
+
export type PermissionConfig = Record<string, PermissionValue> | undefined
|
|
6
|
+
|
|
7
|
+
export interface HostPermissionSnapshot {
|
|
8
|
+
global: PermissionConfig
|
|
9
|
+
agents: Record<string, PermissionConfig>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PermissionRule = {
|
|
13
|
+
permission: string
|
|
14
|
+
pattern: string
|
|
15
|
+
action: PermissionAction
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const findLastMatchingRule = (
|
|
19
|
+
rules: PermissionRule[],
|
|
20
|
+
predicate: (rule: PermissionRule) => boolean,
|
|
21
|
+
): PermissionRule | undefined => {
|
|
22
|
+
for (let index = rules.length - 1; index >= 0; index -= 1) {
|
|
23
|
+
const rule = rules[index]
|
|
24
|
+
if (rule && predicate(rule)) {
|
|
25
|
+
return rule
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const wildcardMatch = (value: string, pattern: string): boolean => {
|
|
33
|
+
const normalizedValue = value.replaceAll("\\", "/")
|
|
34
|
+
let escaped = pattern
|
|
35
|
+
.replaceAll("\\", "/")
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
+
.replace(/\*/g, ".*")
|
|
38
|
+
.replace(/\?/g, ".")
|
|
39
|
+
|
|
40
|
+
if (escaped.endsWith(" .*")) {
|
|
41
|
+
escaped = escaped.slice(0, -3) + "( .*)?"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const flags = process.platform === "win32" ? "si" : "s"
|
|
45
|
+
return new RegExp(`^${escaped}$`, flags).test(normalizedValue)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getPermissionRules = (permissionConfigs: PermissionConfig[]): PermissionRule[] => {
|
|
49
|
+
const rules: PermissionRule[] = []
|
|
50
|
+
for (const permissionConfig of permissionConfigs) {
|
|
51
|
+
if (!permissionConfig) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [permission, value] of Object.entries(permissionConfig)) {
|
|
56
|
+
if (value === "ask" || value === "allow" || value === "deny") {
|
|
57
|
+
rules.push({ permission, pattern: "*", action: value })
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [pattern, action] of Object.entries(value)) {
|
|
62
|
+
if (action === "ask" || action === "allow" || action === "deny") {
|
|
63
|
+
rules.push({ permission, pattern, action })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return rules
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const compressDisabledByOpencode = (...permissionConfigs: PermissionConfig[]): boolean => {
|
|
72
|
+
const match = findLastMatchingRule(getPermissionRules(permissionConfigs), (rule) =>
|
|
73
|
+
wildcardMatch("compress", rule.permission),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return match?.pattern === "*" && match.action === "deny"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const resolveEffectiveCompressPermission = (
|
|
80
|
+
basePermission: PermissionAction,
|
|
81
|
+
hostPermissions: HostPermissionSnapshot,
|
|
82
|
+
agentName?: string,
|
|
83
|
+
): PermissionAction => {
|
|
84
|
+
if (basePermission === "deny") {
|
|
85
|
+
return "deny"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return compressDisabledByOpencode(
|
|
89
|
+
hostPermissions.global,
|
|
90
|
+
agentName ? hostPermissions.agents[agentName] : undefined,
|
|
91
|
+
)
|
|
92
|
+
? "deny"
|
|
93
|
+
: basePermission
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const hasExplicitToolPermission = (
|
|
97
|
+
permissionConfig: PermissionConfig,
|
|
98
|
+
tool: string,
|
|
99
|
+
): boolean => {
|
|
100
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool) : false
|
|
101
|
+
}
|