opencode-secrets-obfuscation 0.1.0 → 0.1.1
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/LICENSE +21 -21
- package/README.md +1 -31
- package/package.json +29 -23
- package/plugin.mjs +3841 -0
- package/.editorconfig +0 -12
- package/.github/workflows/ci.yml +0 -75
- package/.github/workflows/release.yml +0 -50
- package/.node-version +0 -1
- package/AGENTS.md +0 -23
- package/INSTALL.md +0 -104
- package/docs/checkpoint.md +0 -68
- package/docs/history/1-spec-review.md +0 -233
- package/docs/history/10-p0-launch-prerequisites-review.md +0 -186
- package/docs/history/11-p0-job-containment-review.md +0 -188
- package/docs/history/12-p0-api-client.md +0 -15
- package/docs/history/13-p0-status-review.md +0 -122
- package/docs/history/14-p0-containment-final-review.md +0 -84
- package/docs/history/15-config-source-implementation.md +0 -27
- package/docs/history/16-sanitizer-implementation.md +0 -65
- package/docs/history/17-store-policy-implementation.md +0 -36
- package/docs/history/18-pure-mvp-review.md +0 -182
- package/docs/history/19-pure-mvp-re-review.md +0 -142
- package/docs/history/2-p0-runtime-prerequisites.md +0 -163
- package/docs/history/20-pure-mvp-final-review.md +0 -116
- package/docs/history/21-pure-mvp-review-final.md +0 -109
- package/docs/history/22-pure-mvp-signoff-review.md +0 -113
- package/docs/history/23-pure-mvp-signoff-review.md +0 -75
- package/docs/history/24-owned-tools-implementation.md +0 -27
- package/docs/history/25-v2-plugin-integration.md +0 -53
- package/docs/history/26-owned-tools-review.md +0 -138
- package/docs/history/27-owned-tools-final-review.md +0 -93
- package/docs/history/28-owned-tools-signoff.md +0 -98
- package/docs/history/3-p0-harness-foundation.md +0 -105
- package/docs/history/4-p0-source-opening.md +0 -78
- package/docs/history/5-qualification-gate-review.md +0 -133
- package/docs/history/6-p0-infrastructure-review.md +0 -275
- package/docs/history/7-p0-job-containment.md +0 -29
- package/docs/history/8-p0-evidence-support.md +0 -52
- package/docs/history/9-p0-launch-plan.md +0 -88
- package/docs/spec/design.md +0 -111
- package/docs/spec/intent.md +0 -81
- package/docs/spec/plan.md +0 -78
- package/docs/testing.md +0 -157
- package/scripts/build-plugin.mjs +0 -84
- package/scripts/install-opencode-ci.ps1 +0 -39
- package/scripts/install-opencode-ci.sh +0 -27
- package/scripts/smoke.mjs +0 -105
- package/scripts/verify-package.mjs +0 -18
- package/src/README.md +0 -22
- package/src/budgets/index.ts +0 -49
- package/src/config/README.md +0 -4
- package/src/config/options.ts +0 -76
- package/src/diagnostics/README.md +0 -4
- package/src/opencode/v2/filter.ts +0 -350
- package/src/opencode/v2/index.ts +0 -86
- package/src/opencode/v2/runtime.ts +0 -207
- package/src/opencode/v2/transports/README.md +0 -5
- package/src/runtime/http-transport.ts +0 -20
- package/src/sanitize/README.md +0 -4
- package/src/sanitize/index.ts +0 -454
- package/src/sources/README.md +0 -5
- package/src/sources/dotenv.ts +0 -133
- package/src/sources/native/README.md +0 -5
- package/src/store/index.ts +0 -64
- package/tests/README.md +0 -26
- package/tests/contract/plugin-v2.test.ts +0 -29
- package/tests/fixtures/README.md +0 -9
- package/tests/fixtures/job-child.mjs +0 -18
- package/tests/fixtures/plugins/README.md +0 -8
- package/tests/fixtures/process-child.mjs +0 -20
- package/tests/fixtures/services/README.md +0 -10
- package/tests/integration/components.test.ts +0 -5
- package/tests/integration/transparent-runtime.test.ts +0 -97
- package/tests/integration/windows-job.test.ts +0 -71
- package/tests/p0-host/README.md +0 -24
- package/tests/p0-host/linux-job.sh +0 -61
- package/tests/p0-host/windows-job.ps1 +0 -51
- package/tests/p0-native/README.md +0 -33
- package/tests/p0-native/windows-handle-loader.tests.ps1 +0 -191
- package/tests/scaffold/repository.test.ts +0 -105
- package/tests/smoke/README.md +0 -37
- package/tests/smoke/cases.json +0 -17
- package/tests/smoke/qualification.json +0 -20
- package/tests/smoke/runtime-candidate.json +0 -111
- package/tests/support/README.md +0 -32
- package/tests/support/child-process.ts +0 -235
- package/tests/support/host-api.ts +0 -123
- package/tests/support/opencode-environment.ts +0 -239
- package/tests/support/qualification.ts +0 -154
- package/tests/support/recording-endpoint.ts +0 -242
- package/tests/support/run-directory.ts +0 -179
- package/tests/support/smoke-cases.ts +0 -289
- package/tests/unit/budgets.test.ts +0 -35
- package/tests/unit/config.test.ts +0 -40
- package/tests/unit/dotenv.test.ts +0 -72
- package/tests/unit/filter-size.test.ts +0 -132
- package/tests/unit/harness.test.ts +0 -228
- package/tests/unit/host-api.test.ts +0 -122
- package/tests/unit/opencode-environment.test.ts +0 -161
- package/tests/unit/qualification.test.ts +0 -128
- package/tests/unit/recording-endpoint.test.ts +0 -188
- package/tests/unit/sanitize.test.ts +0 -159
- package/tests/unit/smoke-cases.test.ts +0 -20
- package/tsconfig.json +0 -20
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
import { randomBytes } from "node:crypto"
|
|
3
|
-
import * as fs from "node:fs"
|
|
4
|
-
import * as path from "node:path"
|
|
5
|
-
import { Tool } from "@opencode/schema/tool"
|
|
6
|
-
import type { Context } from "@opencode/plugin/effect/plugin"
|
|
7
|
-
import { ActivationStore } from "../../store/index.ts"
|
|
8
|
-
import { BudgetLedger } from "../../budgets/index.ts"
|
|
9
|
-
import { loadDotenvFile, type DotenvSnapshot } from "../../sources/dotenv.ts"
|
|
10
|
-
import { admitPatterns, type Pattern } from "../../sanitize/index.ts"
|
|
11
|
-
import type { PluginOptions } from "../../config/options.ts"
|
|
12
|
-
import type { CoreRuntime, CoreRuntimeFactory } from "./index.ts"
|
|
13
|
-
import {
|
|
14
|
-
WITHHELD,
|
|
15
|
-
containsSecret,
|
|
16
|
-
dropMentions,
|
|
17
|
-
hasSecrets,
|
|
18
|
-
redactHeaders,
|
|
19
|
-
redactRequest,
|
|
20
|
-
redactResponse,
|
|
21
|
-
redactString,
|
|
22
|
-
redactTree,
|
|
23
|
-
type FilterState,
|
|
24
|
-
} from "./filter.ts"
|
|
25
|
-
|
|
26
|
-
const GENERATION = 1
|
|
27
|
-
const SOURCE_TOOLS = new Set(["read", "write", "edit", "patch"])
|
|
28
|
-
const PATH_KEYS = ["filePath", "file_path", "path"]
|
|
29
|
-
|
|
30
|
-
interface LoadedEntry { readonly sourceId: string; readonly key: string; readonly value: string }
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Transparent runtime: load private source values, assign opaque replacements,
|
|
34
|
-
* and sanitize existing OpenCode surfaces. It does not register tools and it
|
|
35
|
-
* never performs network I/O.
|
|
36
|
-
*/
|
|
37
|
-
export const realRuntimeFactory: CoreRuntimeFactory = (
|
|
38
|
-
options: PluginOptions,
|
|
39
|
-
location: Context["location"],
|
|
40
|
-
): Effect.Effect<CoreRuntime, never, never> => {
|
|
41
|
-
const locationDir = location.directory
|
|
42
|
-
let locationReal: string
|
|
43
|
-
try { locationReal = fs.realpathSync(locationDir) } catch { throw new Error("SOURCE_INVALID") }
|
|
44
|
-
const entries: LoadedEntry[] = []
|
|
45
|
-
let totalBytes = 0
|
|
46
|
-
|
|
47
|
-
for (const descriptor of options.sources) {
|
|
48
|
-
const resolved = path.resolve(locationDir, descriptor.path)
|
|
49
|
-
let sourceReal: string
|
|
50
|
-
try { sourceReal = fs.realpathSync(resolved) } catch { if (descriptor.required) throw new Error("SOURCE_INVALID"); continue }
|
|
51
|
-
const relativeSource = path.relative(locationReal, sourceReal)
|
|
52
|
-
if (relativeSource === ".." || relativeSource.startsWith(`..${path.sep}`) || path.isAbsolute(relativeSource)) {
|
|
53
|
-
if (descriptor.required) throw new Error("SOURCE_INVALID")
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
let snapshot: DotenvSnapshot
|
|
57
|
-
try {
|
|
58
|
-
snapshot = loadDotenvFile(resolved, {
|
|
59
|
-
maxBytes: options.limits.sourceBytes,
|
|
60
|
-
maxEntries: options.limits.secretCount,
|
|
61
|
-
maxValueBytes: options.limits.secretBytes,
|
|
62
|
-
})
|
|
63
|
-
} catch {
|
|
64
|
-
if (descriptor.required) throw new Error("SOURCE_INVALID")
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
for (const entry of snapshot.entries) {
|
|
68
|
-
if (!descriptor.keys.includes(entry.key) || entry.value.length === 0) continue
|
|
69
|
-
totalBytes += Buffer.byteLength(entry.value, "utf8")
|
|
70
|
-
if (totalBytes > options.limits.totalSourceBytes || entries.length >= options.limits.secretCount) throw new Error("SOURCE_INVALID")
|
|
71
|
-
entries.push({ sourceId: descriptor.id, key: entry.key, value: entry.value })
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const budget = new BudgetLedger({
|
|
76
|
-
activeSessions: options.limits.activeSessions,
|
|
77
|
-
handles: options.limits.secretCount,
|
|
78
|
-
handlesPerSession: options.limits.handlesPerSession,
|
|
79
|
-
secretCount: options.limits.secretCount,
|
|
80
|
-
secretBytes: options.limits.secretBytes,
|
|
81
|
-
indexBytes: options.limits.indexBytes,
|
|
82
|
-
workingBytes: options.limits.workingBytes,
|
|
83
|
-
})
|
|
84
|
-
const store = new ActivationStore({
|
|
85
|
-
location: { directory: locationDir },
|
|
86
|
-
generation: GENERATION,
|
|
87
|
-
budget,
|
|
88
|
-
randomBytes: () => new Uint8Array(randomBytes(16)),
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const handles = new Map<string, string>()
|
|
92
|
-
for (const entry of entries) if (!handles.has(entry.value)) handles.set(entry.value, store.register(entry.value))
|
|
93
|
-
const patterns: Pattern[] = []
|
|
94
|
-
for (const entry of entries) {
|
|
95
|
-
if (patterns.some((pattern) => pattern.text === entry.value)) continue
|
|
96
|
-
const replacement = handles.get(entry.value)
|
|
97
|
-
if (replacement === undefined) throw new Error("SANITIZATION_UNAVAILABLE")
|
|
98
|
-
patterns.push({ text: entry.value, replacement, entry: entry.value })
|
|
99
|
-
}
|
|
100
|
-
if (!admitPatterns(patterns).ok) throw new Error("SANITIZATION_UNAVAILABLE")
|
|
101
|
-
|
|
102
|
-
const filter: FilterState = {
|
|
103
|
-
patterns,
|
|
104
|
-
budget: {
|
|
105
|
-
maxOutputBytes: Math.min(options.limits.outputBytes, 2 * 1024 * 1024),
|
|
106
|
-
maxDepth: Math.min(options.limits.traversalDepth, 32),
|
|
107
|
-
maxNodes: Math.min(options.limits.traversalNodes, 50_000),
|
|
108
|
-
maxMatches: Math.min(options.limits.traversalNodes, 50_000),
|
|
109
|
-
},
|
|
110
|
-
maxInputBytes: options.limits.inputBytes,
|
|
111
|
-
}
|
|
112
|
-
const scanCeiling = Math.min(filter.maxInputBytes, filter.budget.maxOutputBytes)
|
|
113
|
-
const sourcePaths = new Set(options.sources.map((descriptor) => {
|
|
114
|
-
const resolved = path.resolve(locationDir, descriptor.path)
|
|
115
|
-
return process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
116
|
-
}))
|
|
117
|
-
|
|
118
|
-
const pathInput = (input: unknown): string | undefined => {
|
|
119
|
-
if (!input || typeof input !== "object") return undefined
|
|
120
|
-
const record = input as Record<string, unknown>
|
|
121
|
-
for (const key of PATH_KEYS) {
|
|
122
|
-
const value = record[key]
|
|
123
|
-
if (typeof value === "string" && value.length > 0 && value.length <= 4_096) return value
|
|
124
|
-
}
|
|
125
|
-
return undefined
|
|
126
|
-
}
|
|
127
|
-
const isSourcePath = (candidate: string): boolean => {
|
|
128
|
-
const resolved = path.resolve(locationDir, candidate)
|
|
129
|
-
return sourcePaths.has(process.platform === "win32" ? resolved.toLowerCase() : resolved)
|
|
130
|
-
}
|
|
131
|
-
const inputHasSecret = (input: unknown): boolean => {
|
|
132
|
-
if (!hasSecrets(filter) || input === undefined || input === null) return false
|
|
133
|
-
let serialized: string | undefined
|
|
134
|
-
try { serialized = JSON.stringify(input) } catch { return false }
|
|
135
|
-
return serialized !== undefined && Buffer.byteLength(serialized, "utf8") <= scanCeiling && containsSecret(serialized, filter)
|
|
136
|
-
}
|
|
137
|
-
const redactToolError = (event: { error: Tool.Error }): void => {
|
|
138
|
-
const original = event.error
|
|
139
|
-
const message = typeof original.message === "string" ? redactString(original.message, filter) : null
|
|
140
|
-
event.error = new Tool.Error({ message: message ?? WITHHELD })
|
|
141
|
-
}
|
|
142
|
-
const sanitizeValue = (value: unknown): unknown => {
|
|
143
|
-
if (typeof value === "string") return redactString(value, filter) ?? WITHHELD
|
|
144
|
-
redactTree(value, filter)
|
|
145
|
-
return value
|
|
146
|
-
}
|
|
147
|
-
const sanitizeRequestContext = (event: { system: unknown; messages: unknown; options: unknown; tools?: unknown }): void => {
|
|
148
|
-
event.system = sanitizeValue(event.system)
|
|
149
|
-
redactTree(event.messages, filter)
|
|
150
|
-
event.options = sanitizeValue(event.options)
|
|
151
|
-
if (event.tools !== undefined) event.tools = sanitizeValue(event.tools)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const runtime: CoreRuntime = {
|
|
155
|
-
close: () => Effect.sync(() => store.teardown()),
|
|
156
|
-
session: {
|
|
157
|
-
prompt: (event) => Effect.sync(() => {
|
|
158
|
-
if (!hasSecrets(filter)) return
|
|
159
|
-
let changed = redactTree(event.prompt, filter)
|
|
160
|
-
if (event.metadata !== undefined) changed = redactTree(event.metadata, filter) || changed
|
|
161
|
-
if (changed) dropMentions(event.prompt)
|
|
162
|
-
}),
|
|
163
|
-
context: (event) => Effect.sync(() => { if (hasSecrets(filter)) sanitizeRequestContext(event) }),
|
|
164
|
-
compaction: (event) => Effect.sync(() => {
|
|
165
|
-
if (!hasSecrets(filter)) return
|
|
166
|
-
sanitizeRequestContext(event)
|
|
167
|
-
if (event.result !== undefined) event.result = sanitizeValue(event.result) as typeof event.result
|
|
168
|
-
}),
|
|
169
|
-
generate: (event) => Effect.sync(() => { if (hasSecrets(filter)) sanitizeRequestContext(event) }),
|
|
170
|
-
title: (event) => Effect.sync(() => {
|
|
171
|
-
if (!hasSecrets(filter)) return
|
|
172
|
-
sanitizeRequestContext(event)
|
|
173
|
-
if (event.result !== undefined) event.result = redactString(event.result, filter) ?? WITHHELD
|
|
174
|
-
}),
|
|
175
|
-
modelRequest: (event) => Effect.gen(function* () {
|
|
176
|
-
if (!hasSecrets(filter)) return
|
|
177
|
-
if (redactHeaders(event.headers, filter, { skipAuth: true, strict: true }) === "withheld") yield* Effect.die(new Error(WITHHELD))
|
|
178
|
-
if (event.baseURL !== undefined) event.baseURL = redactString(event.baseURL, filter) ?? WITHHELD
|
|
179
|
-
}),
|
|
180
|
-
httpRequest: (event) => Effect.promise(async () => {
|
|
181
|
-
if (!hasSecrets(filter)) return
|
|
182
|
-
const replacement = await redactRequest(event.request, filter).catch(() => "withheld" as const)
|
|
183
|
-
if (replacement === "withheld") throw new Error(WITHHELD)
|
|
184
|
-
event.request = replacement
|
|
185
|
-
}),
|
|
186
|
-
httpResponse: (event) => Effect.promise(async () => {
|
|
187
|
-
if (!hasSecrets(filter)) return
|
|
188
|
-
try { event.response = await redactResponse(event.response, filter) } catch { event.response = new Response(null, { status: 502 }) }
|
|
189
|
-
}),
|
|
190
|
-
},
|
|
191
|
-
tool: {
|
|
192
|
-
before: (event) => Effect.gen(function* () {
|
|
193
|
-
if (SOURCE_TOOLS.has(event.tool)) {
|
|
194
|
-
const candidate = pathInput(event.input)
|
|
195
|
-
if (candidate !== undefined && isSourcePath(candidate)) yield* Effect.fail(new Tool.Error({ message: "ACCESS_DENIED" }))
|
|
196
|
-
}
|
|
197
|
-
if (inputHasSecret(event.input)) yield* Effect.fail(new Tool.Error({ message: "ACCESS_DENIED" }))
|
|
198
|
-
}),
|
|
199
|
-
after: (event) => Effect.sync(() => {
|
|
200
|
-
if (!hasSecrets(filter)) return
|
|
201
|
-
if (event.status === "completed") event.result = sanitizeValue(event.result) as typeof event.result
|
|
202
|
-
else redactToolError(event)
|
|
203
|
-
}),
|
|
204
|
-
},
|
|
205
|
-
}
|
|
206
|
-
return Effect.succeed(runtime)
|
|
207
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type FetchResult = { readonly code: number }
|
|
2
|
-
|
|
3
|
-
export interface HttpRequest {
|
|
4
|
-
readonly url: string
|
|
5
|
-
readonly method: string
|
|
6
|
-
readonly headers: Record<string, string>
|
|
7
|
-
readonly signal: any
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type CancelableHttpRequest = Promise<FetchResult>
|
|
11
|
-
|
|
12
|
-
export interface HttpTransport {
|
|
13
|
-
request(request: HttpRequest): CancelableHttpRequest
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const nativeHttpTransport: HttpTransport = {
|
|
17
|
-
request(request) {
|
|
18
|
-
return Promise.resolve({ code: 0 })
|
|
19
|
-
},
|
|
20
|
-
}
|
package/src/sanitize/README.md
DELETED