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.
Files changed (104) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1 -31
  3. package/package.json +29 -23
  4. package/plugin.mjs +3841 -0
  5. package/.editorconfig +0 -12
  6. package/.github/workflows/ci.yml +0 -75
  7. package/.github/workflows/release.yml +0 -50
  8. package/.node-version +0 -1
  9. package/AGENTS.md +0 -23
  10. package/INSTALL.md +0 -104
  11. package/docs/checkpoint.md +0 -68
  12. package/docs/history/1-spec-review.md +0 -233
  13. package/docs/history/10-p0-launch-prerequisites-review.md +0 -186
  14. package/docs/history/11-p0-job-containment-review.md +0 -188
  15. package/docs/history/12-p0-api-client.md +0 -15
  16. package/docs/history/13-p0-status-review.md +0 -122
  17. package/docs/history/14-p0-containment-final-review.md +0 -84
  18. package/docs/history/15-config-source-implementation.md +0 -27
  19. package/docs/history/16-sanitizer-implementation.md +0 -65
  20. package/docs/history/17-store-policy-implementation.md +0 -36
  21. package/docs/history/18-pure-mvp-review.md +0 -182
  22. package/docs/history/19-pure-mvp-re-review.md +0 -142
  23. package/docs/history/2-p0-runtime-prerequisites.md +0 -163
  24. package/docs/history/20-pure-mvp-final-review.md +0 -116
  25. package/docs/history/21-pure-mvp-review-final.md +0 -109
  26. package/docs/history/22-pure-mvp-signoff-review.md +0 -113
  27. package/docs/history/23-pure-mvp-signoff-review.md +0 -75
  28. package/docs/history/24-owned-tools-implementation.md +0 -27
  29. package/docs/history/25-v2-plugin-integration.md +0 -53
  30. package/docs/history/26-owned-tools-review.md +0 -138
  31. package/docs/history/27-owned-tools-final-review.md +0 -93
  32. package/docs/history/28-owned-tools-signoff.md +0 -98
  33. package/docs/history/3-p0-harness-foundation.md +0 -105
  34. package/docs/history/4-p0-source-opening.md +0 -78
  35. package/docs/history/5-qualification-gate-review.md +0 -133
  36. package/docs/history/6-p0-infrastructure-review.md +0 -275
  37. package/docs/history/7-p0-job-containment.md +0 -29
  38. package/docs/history/8-p0-evidence-support.md +0 -52
  39. package/docs/history/9-p0-launch-plan.md +0 -88
  40. package/docs/spec/design.md +0 -111
  41. package/docs/spec/intent.md +0 -81
  42. package/docs/spec/plan.md +0 -78
  43. package/docs/testing.md +0 -157
  44. package/scripts/build-plugin.mjs +0 -84
  45. package/scripts/install-opencode-ci.ps1 +0 -39
  46. package/scripts/install-opencode-ci.sh +0 -27
  47. package/scripts/smoke.mjs +0 -105
  48. package/scripts/verify-package.mjs +0 -18
  49. package/src/README.md +0 -22
  50. package/src/budgets/index.ts +0 -49
  51. package/src/config/README.md +0 -4
  52. package/src/config/options.ts +0 -76
  53. package/src/diagnostics/README.md +0 -4
  54. package/src/opencode/v2/filter.ts +0 -350
  55. package/src/opencode/v2/index.ts +0 -86
  56. package/src/opencode/v2/runtime.ts +0 -207
  57. package/src/opencode/v2/transports/README.md +0 -5
  58. package/src/runtime/http-transport.ts +0 -20
  59. package/src/sanitize/README.md +0 -4
  60. package/src/sanitize/index.ts +0 -454
  61. package/src/sources/README.md +0 -5
  62. package/src/sources/dotenv.ts +0 -133
  63. package/src/sources/native/README.md +0 -5
  64. package/src/store/index.ts +0 -64
  65. package/tests/README.md +0 -26
  66. package/tests/contract/plugin-v2.test.ts +0 -29
  67. package/tests/fixtures/README.md +0 -9
  68. package/tests/fixtures/job-child.mjs +0 -18
  69. package/tests/fixtures/plugins/README.md +0 -8
  70. package/tests/fixtures/process-child.mjs +0 -20
  71. package/tests/fixtures/services/README.md +0 -10
  72. package/tests/integration/components.test.ts +0 -5
  73. package/tests/integration/transparent-runtime.test.ts +0 -97
  74. package/tests/integration/windows-job.test.ts +0 -71
  75. package/tests/p0-host/README.md +0 -24
  76. package/tests/p0-host/linux-job.sh +0 -61
  77. package/tests/p0-host/windows-job.ps1 +0 -51
  78. package/tests/p0-native/README.md +0 -33
  79. package/tests/p0-native/windows-handle-loader.tests.ps1 +0 -191
  80. package/tests/scaffold/repository.test.ts +0 -105
  81. package/tests/smoke/README.md +0 -37
  82. package/tests/smoke/cases.json +0 -17
  83. package/tests/smoke/qualification.json +0 -20
  84. package/tests/smoke/runtime-candidate.json +0 -111
  85. package/tests/support/README.md +0 -32
  86. package/tests/support/child-process.ts +0 -235
  87. package/tests/support/host-api.ts +0 -123
  88. package/tests/support/opencode-environment.ts +0 -239
  89. package/tests/support/qualification.ts +0 -154
  90. package/tests/support/recording-endpoint.ts +0 -242
  91. package/tests/support/run-directory.ts +0 -179
  92. package/tests/support/smoke-cases.ts +0 -289
  93. package/tests/unit/budgets.test.ts +0 -35
  94. package/tests/unit/config.test.ts +0 -40
  95. package/tests/unit/dotenv.test.ts +0 -72
  96. package/tests/unit/filter-size.test.ts +0 -132
  97. package/tests/unit/harness.test.ts +0 -228
  98. package/tests/unit/host-api.test.ts +0 -122
  99. package/tests/unit/opencode-environment.test.ts +0 -161
  100. package/tests/unit/qualification.test.ts +0 -128
  101. package/tests/unit/recording-endpoint.test.ts +0 -188
  102. package/tests/unit/sanitize.test.ts +0 -159
  103. package/tests/unit/smoke-cases.test.ts +0 -20
  104. 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,5 +0,0 @@
1
- # Placeholder: model transports
2
-
3
- Qualify one exact native HTTP route while active first. SDK, WebSocket, hosted
4
- tools and independent generation require separate coverage; do not silently count
5
- them as blocked. No implementation yet.
@@ -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
- }
@@ -1,4 +0,0 @@
1
- # Placeholder: sanitization
2
-
3
- Write independent admission/interval/sink-byte oracles first. Implement overlap
4
- unions and post-serialization validation, not leaf-only replacement. No code yet.