chore: merge dev into v2 (#35962)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
840ff14026
commit
51a74c01d5
@@ -8,13 +8,15 @@ import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { agentHost, host } from "../plugin/host"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node])))
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node, Global.node])))
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
const defaultPermissions = [
|
||||
{ action: "*", resource: "*", effect: "allow" },
|
||||
@@ -22,6 +24,33 @@ const defaultPermissions = [
|
||||
] satisfies PermissionV2.Ruleset
|
||||
|
||||
describe("ConfigAgentPlugin.Plugin", () => {
|
||||
it.effect("matches POSIX paths against home-relative permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const permissions = yield* loadHomePermissions("/home/test")
|
||||
expect(PermissionV2.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(PermissionV2.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny")
|
||||
expect(permissions).toContainEqual({ action: "shell", resource: "$HOME/private/**", effect: "deny" })
|
||||
expect(permissions).not.toContainEqual({ action: "shell", resource: "/home/test/private/**", effect: "deny" })
|
||||
expect(PermissionV2.evaluate("shell", "$HOME/private/key", permissions).effect).toBe("deny")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("matches Windows paths against home-relative permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const permissions = yield* loadHomePermissions("C:\\Users\\test")
|
||||
expect(
|
||||
PermissionV2.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect,
|
||||
).toBe("allow")
|
||||
expect(PermissionV2.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe(
|
||||
"deny",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
@@ -280,3 +309,51 @@ Use native v2 fields.`,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
function loadHomePermissions(home: string) {
|
||||
return Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const build = AgentV2.ID.make("build")
|
||||
yield* agents.transform((editor) => editor.update(build, () => {}))
|
||||
const config = Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
permission: {
|
||||
external_directory: {
|
||||
"~/p/**": "allow",
|
||||
"/some/~/path": "deny",
|
||||
"$HOMELESS/**": "deny",
|
||||
},
|
||||
bash: {
|
||||
"$HOME/private/**": "deny",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
build: {
|
||||
permission: {
|
||||
external_directory: {
|
||||
"$HOME/cache/**": "deny",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })),
|
||||
)
|
||||
|
||||
const agent = yield* agents.get(build)
|
||||
if (!agent) throw new Error("expected configured build agent")
|
||||
return agent.permissions
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,35 +185,33 @@ describeWatcher("LocationWatcher", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("watches non-git roots", () =>
|
||||
it.live("skips non-git roots", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const file = path.join(directory, "plain.txt")
|
||||
yield* ready(directory)
|
||||
expect(yield* nextUpdate((event) => event.file === file, fs.writeFileString(file, "plain"))).toEqual({
|
||||
file,
|
||||
event: "add",
|
||||
})
|
||||
yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "plain"))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("ignores dependency, VCS, and build directories at any depth", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const afs = yield* FSUtil.Service
|
||||
yield* ready(directory)
|
||||
const roots = ["node_modules", ".git", "dist"].map((name) => path.join(directory, "nested", name))
|
||||
const files = roots.map((root) => path.join(root, "package", "index.js"))
|
||||
yield* noUpdate(
|
||||
(event) => roots.some((root) => event.file === root || event.file.startsWith(`${root}${path.sep}`)),
|
||||
Effect.forEach(files, (file) => afs.writeWithDirs(file, "ignored"), {
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
withTmp(
|
||||
(directory) =>
|
||||
Effect.gen(function* () {
|
||||
const afs = yield* FSUtil.Service
|
||||
yield* ready(directory)
|
||||
const roots = ["node_modules", ".git", "dist"].map((name) => path.join(directory, "nested", name))
|
||||
const files = roots.map((root) => path.join(root, "package", "index.js"))
|
||||
yield* noUpdate(
|
||||
(event) => roots.some((root) => event.file === root || event.file.startsWith(`${root}${path.sep}`)),
|
||||
Effect.forEach(files, (file) => afs.writeWithDirs(file, "ignored"), {
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -225,7 +223,10 @@ describeWatcher("LocationWatcher", () => {
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* ready(tmp.path).pipe(provide(tmp.path), Effect.scoped)
|
||||
yield* ready(tmp.path).pipe(
|
||||
provide(tmp.path, { type: "git", store: AbsolutePath.make(path.join(tmp.path, ".git")) }),
|
||||
Effect.scoped,
|
||||
)
|
||||
const file = path.join(tmp.path, "after-dispose.txt")
|
||||
yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe(
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
|
||||
Reference in New Issue
Block a user