Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to requ

We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.

To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/anomalyco/opencode/security/advisories/new) tab.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/XiaomiMiMo/MiMo-Code/security/advisories/new) tab.

The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.

Expand Down
9 changes: 9 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ export const layer = Layer.effect(
"*.env.*": "ask",
"*.env.example": "allow",
},
// Deleting files is irreversible, so require confirmation even though
// bash defaults to allow. Specific rules come after "*": "allow" so
// findLast makes them win (#2073).
bash: {
"*": "allow",
"rm *": "ask",
"del *": "ask",
"remove-item *": "ask",
},
})

const user = Permission.fromConfig(cfg.permission ?? {})
Expand Down
48 changes: 28 additions & 20 deletions packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,33 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
}
}

export function mcpDebugAuthLines(authStatus: MCP.AuthStatus, entry?: McpAuth.Entry) {
return [
`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`,
...(!entry?.tokens
? []
: [
` Access token: present`,
...(entry.tokens.expiresAt
? [
` Expires: ${new Date(entry.tokens.expiresAt * 1000).toISOString()} ${
entry.tokens.expiresAt < Date.now() / 1000 ? "(EXPIRED)" : ""
}`,
]
: []),
...(entry.tokens.refreshToken ? [` Refresh token: present`] : []),
]),
...(!entry?.clientInfo
? []
: [
` Client ID: ${entry.clientInfo.clientId}`,
...(entry.clientInfo.clientSecretExpiresAt
? [` Client secret expires: ${new Date(entry.clientInfo.clientSecretExpiresAt * 1000).toISOString()}`]
: []),
]),
]
}

type McpEntry = NonNullable<Config.Info["mcp"]>[string]

type McpConfigured = ConfigMCP.Info
Expand Down Expand Up @@ -675,26 +702,7 @@ export const McpDebugCommand = cmd({
}
}),
)
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)

if (entry?.tokens) {
prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
if (entry.tokens.expiresAt) {
const expiresDate = new Date(entry.tokens.expiresAt * 1000)
const isExpired = entry.tokens.expiresAt < Date.now() / 1000
prompts.log.info(` Expires: ${expiresDate.toISOString()} ${isExpired ? "(EXPIRED)" : ""}`)
}
if (entry.tokens.refreshToken) {
prompts.log.info(` Refresh token: present`)
}
}
if (entry?.clientInfo) {
prompts.log.info(` Client ID: ${entry.clientInfo.clientId}`)
if (entry.clientInfo.clientSecretExpiresAt) {
const expiresDate = new Date(entry.clientInfo.clientSecretExpiresAt * 1000)
prompts.log.info(` Client secret expires: ${expiresDate.toISOString()}`)
}
}
mcpDebugAuthLines(authStatus, entry).forEach((line) => prompts.log.info(line))

const spinner = prompts.spinner()
spinner.start("Testing connection...")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
import { pathToFileURL } from "bun"
import fuzzysort from "fuzzysort"
import { firstBy } from "remeda"
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
Expand All @@ -23,6 +22,11 @@ import { useFrecency } from "./frecency"
import { detectTrigger, exactSubmitOption } from "./autocomplete-detect"
import { charAfterCursor, tokenEndWidth } from "./offset"

export function padAutocompleteDisplay(labels: string[]) {
const max = Math.max(...labels.map((label) => Bun.stringWidth(label)), 0)
return labels.map((label) => label + " ".repeat(Math.max(0, max + 2 - Bun.stringWidth(label))))
}

function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
Expand Down Expand Up @@ -424,11 +428,11 @@ export function Autocomplete(props: {

results.sort((a, b) => a.display.localeCompare(b.display))

const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
if (!max) return results
return results.map((item) => ({
const displays = padAutocompleteDisplay(results.map((item) => item.display))
if (!displays.length) return results
return results.map((item, index) => ({
...item,
display: item.display.padEnd(max + 2),
display: displays[index]!,
}))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
import { Instance } from "@/project/instance"
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util"
import { timingSafeStringEqual } from "@/util/crypto"
import { ConfigApi, configHandlers } from "./config"
import { PermissionApi, permissionHandlers } from "./permission"
import { ProjectApi, projectHandlers } from "./project"
Expand Down Expand Up @@ -77,7 +78,7 @@ const auth = Layer.succeed(
if (credential.username !== user) {
return yield* new Unauthorized({ message: "Unauthorized" })
}
if (Redacted.value(credential.password) !== Flag.MIMOCODE_SERVER_PASSWORD) {
if (!timingSafeStringEqual(Redacted.value(credential.password), Flag.MIMOCODE_SERVER_PASSWORD)) {
return yield* new Unauthorized({ message: "Unauthorized" })
}
return yield* effect
Expand Down
41 changes: 38 additions & 3 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ import { Config } from "@/config"
import { NotFoundError } from "@/storage"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Layer, Context } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { InstanceState } from "@/effect"
import { isOverflow as overflow, usable } from "./overflow"
import { makeRuntime } from "@/effect/run-service"
import { fn } from "@/util/fn"
import { buildLLMRequestPrefix } from "./llm-request-prefix"
import { SystemPrompt } from "./system"
import { Instruction } from "./instruction"
import { LLM } from "./llm"
import { ToolRegistry } from "../tool"
import { AppFileSystem } from "@mimo-ai/shared/filesystem"

const log = Log.create({ service: "session.compaction" })

Expand Down Expand Up @@ -108,6 +115,10 @@ export const layer: Layer.Layer<
| Plugin.Service
| SessionProcessor.Service
| Provider.Service
| SystemPrompt.Service
| Instruction.Service
| LLM.Service
| ToolRegistry.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
Expand All @@ -118,6 +129,10 @@ export const layer: Layer.Layer<
const plugin = yield* Plugin.Service
const processors = yield* SessionProcessor.Service
const provider = yield* Provider.Service
const system = yield* SystemPrompt.Service
const instruction = yield* Instruction.Service
const llm = yield* LLM.Service
const toolRegistry = yield* ToolRegistry.Service

const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
tokens: MessageV2.Assistant["tokens"]
Expand Down Expand Up @@ -333,6 +348,18 @@ export const layer: Layer.Layer<
const msgs = structuredClone(selected.head)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true })
const parentAgent = yield* agents.get(userMessage.agent)
const [env, instructions] = yield* Effect.all([
system.environment(model, userMessage.time.created),
instruction.system().pipe(Effect.orDie),
])
const prefix = yield* buildLLMRequestPrefix({
sessionID: input.sessionID,
agent: parentAgent,
model,
msgs,
additions: [...env, ...instructions.content],
}).pipe(Effect.provideService(LLM.Service, llm), Effect.provideService(ToolRegistry.Service, toolRegistry))
const ctx = yield* InstanceState.context
const msg: MessageV2.Assistant = {
id: MessageID.ascending(),
Expand Down Expand Up @@ -369,10 +396,12 @@ export const layer: Layer.Layer<
})
const result = yield* processor.process({
user: userMessage,
agent,
agent: parentAgent,
sessionID: input.sessionID,
tools: {},
system: [],
tools: prefix.tools,
system: [...env, ...instructions.content],
prebuiltSystem: prefix.system,
toolChoice: "none",
messages: [
...modelMessages,
{
Expand Down Expand Up @@ -544,6 +573,12 @@ export const layer: Layer.Layer<

export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(ToolRegistry.defaultLayer),
Layer.provide(LLM.defaultLayer),
Layer.provide(Instruction.layer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionProcessor.defaultLayer),
Expand Down
Loading