oc-one-guard 0.1.1 → 0.2.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/README.md +36 -3
- package/bin/oc-one-guard-doctor +37 -0
- package/config.example.json +2 -6
- package/docs/README.md +3 -1
- package/eval/runner.js +2 -2
- package/index.js +7 -214
- package/package.json +4 -3
- package/src/adapters/v1.js +39 -0
- package/src/adapters/v2.js +67 -0
- package/src/audit.js +2 -0
- package/src/config.js +24 -20
- package/src/defaults.js +18 -9
- package/src/guard.js +41 -22
- package/src/judge.js +47 -44
- package/src/runtime.js +254 -0
- package/src/state.js +21 -23
- package/src/tools.js +37 -0
package/src/state.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { getWrite, referencedWrites } from "./written.js"
|
|
20
20
|
import { requireScope } from "./scope.js"
|
|
21
|
+
import { canonicalTool, pathsOf } from "./tools.js"
|
|
21
22
|
|
|
22
23
|
const URL_PATTERN = /\b(?:https?|ftps?|sftp|scp|rsync|ssh|git):\/\/[^\s"'`<>|;&]+/gi
|
|
23
24
|
|
|
@@ -51,7 +52,7 @@ const LOCAL_ONLY_TOOLS = new Set([
|
|
|
51
52
|
* file and execute it in a later call. Their content is judged when it
|
|
52
53
|
* contains something that moves data.
|
|
53
54
|
*/
|
|
54
|
-
const CONTENT_TOOLS = new Set(["write", "edit", "
|
|
55
|
+
const CONTENT_TOOLS = new Set(["write", "edit", "patch"])
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Primitives that reach the network, matched against shell text.
|
|
@@ -138,13 +139,14 @@ export function runsLocalScript(text) {
|
|
|
138
139
|
* @returns {string[]}
|
|
139
140
|
*/
|
|
140
141
|
export function extractHosts(tool, args) {
|
|
142
|
+
const name = canonicalTool(tool)
|
|
141
143
|
const chunks = []
|
|
142
|
-
if (
|
|
144
|
+
if (name === "shell") {
|
|
143
145
|
if (typeof args?.command === "string") chunks.push(args.command)
|
|
144
|
-
} else if (
|
|
146
|
+
} else if (name === "webfetch") {
|
|
145
147
|
if (typeof args?.url === "string") chunks.push(args.url)
|
|
146
|
-
} else if (CONTENT_TOOLS.has(
|
|
147
|
-
chunks.push(writtenContent(
|
|
148
|
+
} else if (CONTENT_TOOLS.has(name)) {
|
|
149
|
+
chunks.push(writtenContent(name, args))
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
const hosts = new Set()
|
|
@@ -170,20 +172,14 @@ export function extractPaths(tool, args) {
|
|
|
170
172
|
const push = (value) => {
|
|
171
173
|
if (typeof value === "string" && value.trim()) paths.push(value.trim())
|
|
172
174
|
}
|
|
173
|
-
switch (tool) {
|
|
175
|
+
switch (canonicalTool(tool)) {
|
|
174
176
|
case "read":
|
|
175
|
-
push(args?.filePath)
|
|
176
|
-
break
|
|
177
177
|
case "write":
|
|
178
|
-
push(args?.filePath)
|
|
179
|
-
break
|
|
180
178
|
case "edit":
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
case "apply_patch":
|
|
184
|
-
push(args?.filePath)
|
|
179
|
+
case "patch":
|
|
180
|
+
pathsOf(args).forEach(push)
|
|
185
181
|
break
|
|
186
|
-
case "
|
|
182
|
+
case "shell":
|
|
187
183
|
push(args?.workdir)
|
|
188
184
|
break
|
|
189
185
|
default:
|
|
@@ -287,15 +283,16 @@ export function fullText(tool, args) {
|
|
|
287
283
|
* helper is used so the two agree.
|
|
288
284
|
*/
|
|
289
285
|
export function writtenContent(tool, args) {
|
|
286
|
+
const name = canonicalTool(tool)
|
|
290
287
|
if (!args || typeof args !== "object") return ""
|
|
291
|
-
if (
|
|
292
|
-
if (
|
|
288
|
+
if (name === "write") return typeof args.content === "string" ? args.content : ""
|
|
289
|
+
if (name === "edit") {
|
|
293
290
|
const parts = []
|
|
294
291
|
if (typeof args.newString === "string") parts.push(args.newString)
|
|
295
292
|
if (typeof args.oldString === "string") parts.push(args.oldString)
|
|
296
293
|
return parts.join("\n")
|
|
297
294
|
}
|
|
298
|
-
if (
|
|
295
|
+
if (name === "patch") {
|
|
299
296
|
const parts = []
|
|
300
297
|
if (typeof args.patchText === "string") parts.push(args.patchText)
|
|
301
298
|
if (typeof args.patch === "string") parts.push(args.patch)
|
|
@@ -316,17 +313,18 @@ export function writtenContent(tool, args) {
|
|
|
316
313
|
*/
|
|
317
314
|
export function hasOutboundSurface(tool, args, directory, scope) {
|
|
318
315
|
requireScope(scope)
|
|
319
|
-
|
|
316
|
+
const name = canonicalTool(tool)
|
|
317
|
+
if (LOCAL_ONLY_TOOLS.has(name)) {
|
|
320
318
|
// A file write can stage a payload that a later call executes, so the
|
|
321
319
|
// content decides.
|
|
322
|
-
if (CONTENT_TOOLS.has(
|
|
323
|
-
const content = writtenContent(
|
|
320
|
+
if (CONTENT_TOOLS.has(name)) {
|
|
321
|
+
const content = writtenContent(name, args)
|
|
324
322
|
return hasTransferPrimitive(content)
|
|
325
323
|
}
|
|
326
324
|
return false
|
|
327
325
|
}
|
|
328
326
|
|
|
329
|
-
if (
|
|
327
|
+
if (name === "shell") {
|
|
330
328
|
const command = typeof args?.command === "string" ? args.command : ""
|
|
331
329
|
if (hasTransferPrimitive(command)) return true
|
|
332
330
|
if (runsLocalScript(command)) return true
|
|
@@ -336,7 +334,7 @@ export function hasOutboundSurface(tool, args, directory, scope) {
|
|
|
336
334
|
return false
|
|
337
335
|
}
|
|
338
336
|
|
|
339
|
-
if (
|
|
337
|
+
if (name === "webfetch") {
|
|
340
338
|
if (args && Object.keys(args).some((key) => !["url", "format", "timeout"].includes(key))) return true
|
|
341
339
|
// A GET can still carry data in the path or the query string.
|
|
342
340
|
const url = typeof args?.url === "string" ? args.url : ""
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool identifiers shared by OpenCode 1.x and 2.x.
|
|
3
|
+
*
|
|
4
|
+
* Canonical names are the OpenCode 2.x ids. Names from 1.x are mapped onto
|
|
5
|
+
* them, so policy lists and checks work for both hosts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ALIASES = new Map([
|
|
9
|
+
["bash", "shell"],
|
|
10
|
+
["apply_patch", "patch"],
|
|
11
|
+
["task", "subagent"],
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
export function canonicalTool(name) {
|
|
15
|
+
if (typeof name !== "string") return name
|
|
16
|
+
return ALIASES.get(name) ?? name
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isMutationTool(tool) {
|
|
20
|
+
return ["write", "edit", "patch"].includes(canonicalTool(tool))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function listsTool(list, tool) {
|
|
24
|
+
const wanted = canonicalTool(tool)
|
|
25
|
+
return list.some(entry => typeof entry === "string" && canonicalTool(entry) === wanted)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 1.x write, edit and read take `filePath`; 2.x takes `path`. Every listed
|
|
29
|
+
// path counts as evidence, so a call that carries both is judged on both.
|
|
30
|
+
export function pathsOf(args) {
|
|
31
|
+
const paths = []
|
|
32
|
+
for (const key of ["filePath", "path"]) {
|
|
33
|
+
const value = args?.[key]
|
|
34
|
+
if (typeof value === "string" && value && !paths.includes(value)) paths.push(value)
|
|
35
|
+
}
|
|
36
|
+
return paths
|
|
37
|
+
}
|