opencode-secrets-obfuscation 0.1.0 → 0.1.2

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 +11 -21
  3. package/package.json +29 -23
  4. package/plugin.mjs +3847 -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
package/docs/spec/plan.md DELETED
@@ -1,78 +0,0 @@
1
- # Implementation plan: transparent OpenCode V2 substitution
2
-
3
- Status: authoritative backlog, revised 2026-10-16.
4
-
5
- ## Delivery rule
6
-
7
- Implement and test the transparent substitution boundary first. Do not add
8
- credential tools, capability brokers, owned HTTP operations, endpoint
9
- connectors, DNS pinning, or authorization flows: they are outside this MVP.
10
-
11
- A case is not complete because the pure sanitizer passes. It needs a sink-level
12
- or real-host test for the exact bytes the host publishes.
13
-
14
- ## Current status
15
-
16
- - [x] Native V2 Effect entrypoint loads from a configured plugin directory.
17
- - [x] Pinned development dependencies: `@opencode/plugin` 2.0.28 and Effect 4.0.1.
18
- - [x] Relative dotenv source parsing with explicit key allowlist.
19
- - [x] Private value-to-opaque-handle map with bounded lifetime.
20
- - [x] Bounded recursive redaction/substitution primitives.
21
- - [x] Existing session/model/http/tool hooks wired without tool registration.
22
- - [x] `P5-TRANSPARENT-ROUNDTRIP` real-host E2E passes on OpenCode 2.0.28.
23
- - [x] `P5-SINKS` proves request substitution and structurally valid SSE response substitution on the real host.
24
- - [x] Direct integration coverage exercises all declared session/model/http/tool hook adapters with synthetic values.
25
- - [ ] Complete sink matrix and release qualification.
26
-
27
- ## MVP backlog
28
-
29
- ### M1 — Source and admission integrity
30
-
31
- - [ ] Replace path-only source opening with the intended validated opened-handle
32
- loader for each advertised filesystem, or explicitly limit the MVP to the
33
- filesystem proven safe.
34
- - [ ] Test malformed dotenv, duplicate keys, unsafe values, representation
35
- collisions and all-or-nothing admission.
36
- - [ ] Test activation close, source rotation and stale map disposal.
37
-
38
- ### M2 — Existing OpenCode sink coverage
39
-
40
- - [x] Prompt/user messages and metadata on the initial provider path.
41
- - [x] Context/system messages, compaction and generate.
42
- - [x] Title requests and title results.
43
- - [x] Model request headers/options/base URL and final serialized provider body.
44
- - [x] Tool input before-hooks and completed/error after-hooks.
45
- - [ ] Structured output, scalar values, metadata, newline joins and cross-part
46
- reconstruction.
47
- - [ ] Logs, progress, persistence, snapshots, queue/overflow files and other
48
- durable sinks that the selected host actually exposes.
49
-
50
- ### M3 — Bounds and failure behavior
51
-
52
- - [ ] Expansion and traversal limits with no partial publication.
53
- - [ ] Oversized bodies, malformed/opaque payloads and cancellation.
54
- - [ ] Concurrent sessions/activations and aggregate accounting.
55
- - [ ] Plugin failure/unload behavior documented as uncovered unless a host gate
56
- independently blocks publication.
57
-
58
- ### M4 — Packaging and claims
59
-
60
- - [ ] Publish an active-plugin-only compatibility matrix and explicit gaps for
61
- the one supported active-hook/provider boundary.
62
- - [ ] Verify packed-artifact installation, restart and clean-environment upgrade.
63
-
64
- ## Explicitly not part of this MVP
65
-
66
- These are not backlog items for this product: host-owned required-plugin
67
- readiness, mandatory route blocking after unload, host-mediated permission
68
- enforcement, sandboxed credential execution, vault leases, durable policy
69
- revisions, egress policy, DNS pinning, proxy policy, credential transports and
70
- any plugin-owned authenticated network request.
71
-
72
- ## Exit gate
73
-
74
- The transparent MVP is complete only when the declared active-plugin envelope
75
- has exact sink evidence, source-opening evidence for every advertised platform,
76
- bounded failure tests, a clean packed-artifact E2E run, and documentation that
77
- names every uncovered route. It must not be called universally protective or
78
- enforced.
package/docs/testing.md DELETED
@@ -1,157 +0,0 @@
1
- # Test-first development
2
-
3
- Status: P0 test infrastructure in progress; the product plugin MVP now exists and
4
- `P5-TRANSPARENT-ROUNDTRIP` executes against the pinned real host (opencode 2.0.28) inside a
5
- private sandbox (`tests/support/smoke-cases.ts`). Test support includes a
6
- scripted loopback recorder (resolver, dynamic bodies, header captures), a
7
- validated sandbox config channel, and evidence bookkeeping; the native
8
- source-opening spike is explicitly unqualified and no runtime is qualified for
9
- release. See [runtime prerequisites](history/2-p0-runtime-prerequisites.md),
10
- [source-opening status](history/4-p0-source-opening.md), and the fresh-eye reviews
11
- under `docs/history/`. Helpers and source probes are not P0 gate completion.
12
-
13
- ## Local commands
14
-
15
- Use Node **24.14.1** (`.node-version`) and npm. This is the development test runtime,
16
- not a selection of OpenCode's host/Effect versions.
17
-
18
- ```sh
19
- npm ci
20
- npm run check
21
- npm run test:smoke:list
22
- ```
23
-
24
- On PowerShell, use `npm.cmd` if script execution policy blocks `npm.ps1`.
25
-
26
- | Command | What it establishes today |
27
- | --- | --- |
28
- | `npm run typecheck` | Product sources and tests typecheck. |
29
- | `npm test` | Scaffold, unit, contract and integration suites plus the smoke runner contract execute. Passing suites are not release qualification. |
30
- | `npm run test:unit` | Product algorithm/policy suites plus test-helper tests. |
31
- | `npm run test:contract` | Pinned host data/lifecycle contracts that do not need a live host. |
32
- | `npm run test:integration` | Controlled-service tests: transport behavior and the runtime factory against loopback verifiers. |
33
- | `npm run test:smoke:list` | Lists stable smoke case IDs without executing anything. |
34
- | `npm run test:smoke -- --run P5-TRANSPARENT-ROUNDTRIP` | Executes the bound real-host case against the pinned host in a private sandbox; unimplemented selections still exit 2. |
35
- | `npm run test:qualification` | Fails nonzero: nothing is qualified (underlying runner exits 2). |
36
-
37
- Do not turn missing prerequisites into successful skips in the future smoke gate.
38
- A developer can run a platform subset, but its report must name excluded cases
39
- and cannot qualify the full platform matrix.
40
-
41
- ## First implementation sequence
42
-
43
- 1. **P0 fixtures before host qualification.** Pin published OpenCode, plugin API, Effect,
44
- provider protocol and per-platform binary checksums in
45
- `tests/smoke/qualification.json`. Keep `status: unqualified` until evidence
46
- actually satisfies the selected qualification profile. Do not infer pins from
47
- the operator's current installation.
48
- - On Linux/ext4, the source-opening spike validates file/ancestor traversal,
49
- reparse/symlink/hard-link rejection, and ACL/owner rights using the
50
- `linux-job.sh` containment test (`tests/p0-host/linux-job.sh`). Qualify
51
- aggregate quota sharing or explicitly choose the dedicated-service restriction.
52
- 2. **Write the isolated service harness.** Add explicit-path process launch,
53
- private config/data/home, readiness probes, safe evidence, timeouts and
54
- owned-process cleanup in `tests/support/`. First real test: `P0-BOOT` calls the
55
- real service API and verifies artifact identity. Its assertions must fail for
56
- an absent/wrong binary, bad version, timeout and accidental shared-service use.
57
- 3. **Add test-only V2 probe plugins.** Populate `tests/fixtures/plugins/` to measure
58
- hook execution, typed errors, unload/fallback and serialization before the
59
- product exists. Drive `P0-HOOKS` and `P0-ABORT` using a recording provider. Keep
60
- response/transport observations distinct from assumptions in mock contracts.
61
- 4. **Prove source-opening and quota feasibility.** Real Windows/NTFS and
62
- Linux/ext4 tests come before choosing native dependencies. Qualify aggregate
63
- quota sharing or explicitly choose the dedicated-service restriction.
64
- 5. **Write red tests and independent oracles.** Replace named TODOs in
65
- `tests/unit/`, `tests/contract/` and `tests/integration/` with concrete failures.
66
- Cover all [R1–R8 remediation gates](spec/plan.md#review-remediation-gates).
67
- Only then implement the matching `src/` module.
68
- 6. **Reuse the harness for host qualification.** When a real entrypoint exists, pack and
69
- install the actual artifact into the isolated host. `P5-TRANSPARENT-ROUNDTRIP`
70
- verifies that existing provider traffic contains an opaque replacement, that
71
- no plugin-owned tool or network request exists, and that the raw source value
72
- stays out of provider bytes and public output. Follow with sink/spill, body
73
- withholding and resource cases.
74
-
75
- The plugin MVP builds from `src/` against pinned `@opencode/plugin`/`effect`
76
- devDependencies and executes inside the real host via `P5-TRANSPARENT-ROUNDTRIP`. The
77
- plugin-only MVP and production profiles remain unqualified until their
78
- applicable real-host gates pass; `tests/smoke/qualification.json` still carries
79
- null pins on purpose.
80
-
81
- ## Transparent substitution boundary
82
-
83
- The plugin is deliberately not a credential broker. It loads configured source
84
- values into a private in-memory map, assigns opaque `secret-<uuid>` replacements,
85
- and applies those replacements through existing OpenCode session/model/http/tool
86
- hooks. It does not add model-visible tools and does not initiate HTTP requests.
87
-
88
- `P5-TRANSPARENT-ROUNDTRIP` proves the first boundary only: an existing provider
89
- request is captured with the opaque replacement and without the raw source value.
90
- Additional sink and route cases are required before making broad "everything"
91
- protection claims.
92
-
93
- ## Test layers and proof quality
94
-
95
- - **Unit:** pure input/output and lifecycle state machines with independent
96
- expected results. Use analytical rejected values for algorithm tests separately
97
- from accepted-snapshot end-to-end cases.
98
- - **Contract:** pin the host schemas and transformation graph. Replay captured
99
- synthetic shapes, but do not substitute these tests for real host behavior.
100
- - **Integration:** use the actual selected filesystem/network/native libraries,
101
- bounded controlled HTTPS services, real cancellation and allocation behavior.
102
- - **Real smoke:** real pinned OpenCode subprocess + API + test-only providers and
103
- endpoints. This is a real host smoke test even without paid/live model calls.
104
- Merely constructing a fake `ctx` does not qualify.
105
- - **Qualification:** require implemented, executed, non-skipped cases for a
106
- declared profile/platform/artifact set, with actual sink/network evidence.
107
- `expected-gap` observations never satisfy enforced-profile requirements.
108
-
109
- Smoke case IDs in `tests/smoke/cases.json` are stable review references. They are
110
- not a claim of exhaustive coverage: expand the inventory as P0 exposes more
111
- paths. The future runner must bind IDs to real test executables and reject unknown
112
- or empty selections, missing implementations and stale evidence.
113
-
114
- The current tests under `tests/scaffold/` intentionally enforce the placeholder
115
- state. Evolve those assertions when a real harness replaces it; replace them with
116
- negative tests proving the new harness still rejects missing pins and cases.
117
- Never simply delete the fail-closed checks to make CI green.
118
-
119
- ## Isolation and evidence rules
120
-
121
- Before launching any real host, prove its config/home/data/cache and service
122
- discovery are isolated on the pinned release. Environment variable names and CLI
123
- flags must come from that release's API/docs, not guesses. Until proven, fail
124
- before launch. Do not reuse `~/.config/opencode`, user sessions, inherited API
125
- keys, credential agents, or the default background service. Pass an explicit
126
- allowlist of essential child environment values and synthetic fixture settings.
127
-
128
- The future harness creates `.artifacts/<run-id>/` with private ACL/mode and an
129
- ownership marker before writing any fixture or capture. The root is ignored by
130
- Git, but that does not prevent backups/sync or grant confidentiality. Raw output,
131
- provider payloads, DBs, source files and failure captures stay there, never in
132
- ordinary CI logs/uploads. Print only validated case IDs, counts, status and safe
133
- error codes. Validate a capture before promoting anything to a public report.
134
-
135
- Generate TLS private keys and synthetic canaries per run. Do not commit them.
136
- Cleanup must use exact validated owned paths/PIDs, without broad recursive deletes
137
- or `killall` behavior. Preserve failed evidence only with explicit private
138
- retention; do not include it in a published package or automatic artifact upload.
139
-
140
- Each future result records: case ID, expectation, profile, implementation status,
141
- actual host/plugin/provider/checksum identities, OS/filesystem, request kind,
142
- observed pass/fail/gap, and restricted evidence references. Denial tests assert
143
- zero resolutions and zero outbound attempts; absence of token text alone is not
144
- enough. Resource tests measure real peak allocation; overlap tests use a separate
145
- interval oracle; reload tests reacquire **new** handles after failed replacement.
146
-
147
- ## CI policy
148
-
149
- The initial `scaffold` workflow runs install, typecheck and scaffold/TODO suites on
150
- Windows and Linux. It deliberately does not launch OpenCode, upload artifacts,
151
- receive credentials, or label its outcome a smoke/security pass.
152
-
153
- Add the real smoke workflow only when isolation/cleanup and exact artifacts are
154
- implemented. Run it with no production secrets or inherited credentials; use
155
- ephemeral workers for OS/network adversarial cases. Never expose repository
156
- secrets to untrusted pull-request code. Enforced-profile CI remains separate from
157
- the active-only MVP matrix.
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { build } from "esbuild"
4
- import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"
5
- import { join, dirname } from "node:path"
6
- import { fileURLToPath } from "node:url"
7
-
8
- const __filename = fileURLToPath(import.meta.url)
9
- const ROOT = dirname(dirname(__filename))
10
- const DIST = join(ROOT, "dist")
11
- const ENTRY = join(ROOT, "src", "opencode", "v2", "index.ts")
12
-
13
- async function buildPlugin() {
14
- console.log("Building plugin...")
15
-
16
- await rm(DIST, { recursive: true, force: true })
17
- await mkdir(DIST, { recursive: true })
18
-
19
- const result = await build({
20
- entryPoints: [ENTRY],
21
- bundle: true,
22
- platform: "node",
23
- format: "esm",
24
- target: "node24",
25
- outfile: join(DIST, "plugin.mjs"),
26
- external: [
27
- "effect"
28
- ],
29
- sourcemap: process.env.PLUGIN_SOURCEMAP === "true",
30
- minify: false,
31
- logLevel: "info"
32
- })
33
-
34
- if (result.errors.length > 0) {
35
- console.error("Build failed:", result.errors)
36
- process.exit(1)
37
- }
38
-
39
- const version = process.env.PLUGIN_VERSION ?? "0.0.0-dev"
40
- if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
41
- throw new Error("PLUGIN_VERSION_INVALID")
42
- }
43
-
44
- // Create a publication-only package. The private repository root is never published.
45
- const pkg = {
46
- name: "opencode-secrets-obfuscation",
47
- version,
48
- description: "Transparent OpenCode V2 secret substitution plugin",
49
- license: "MIT",
50
- repository: "github:jfomhover/opencode-secrets-obfuscation",
51
- homepage: "https://github.com/jfomhover/opencode-secrets-obfuscation",
52
- bugs: "https://github.com/jfomhover/opencode-secrets-obfuscation/issues",
53
- keywords: ["opencode", "opencode-plugin", "secrets", "redaction", "privacy"],
54
- type: "module",
55
- engines: { node: ">=24.14.0 <25" },
56
- main: "plugin.mjs",
57
- exports: {
58
- ".": "./plugin.mjs"
59
- },
60
- files: ["plugin.mjs", "README.md", "LICENSE"],
61
- peerDependencies: { effect: "4.0.1" },
62
- }
63
- await writeFile(join(DIST, "package.json"), JSON.stringify(pkg, null, 2))
64
- await copyFile(join(ROOT, "LICENSE"), join(DIST, "LICENSE"))
65
- const readme = [
66
- "# opencode-secrets-obfuscation",
67
- "",
68
- "Transparent OpenCode V2 secret substitution. The plugin loads explicitly configured dotenv values privately and substitutes opaque secret-<uuid> handles through existing OpenCode hooks. It does not register model-visible tools or make plugin-owned network requests.",
69
- "",
70
- `Install with: opencode plugin add opencode-secrets-obfuscation@${version}`,
71
- "Configure the published package in opencode.jsonc with the options documented at https://github.com/jfomhover/opencode-secrets-obfuscation/blob/main/INSTALL.md.",
72
- "",
73
- ].join("\\n")
74
- await writeFile(join(DIST, "README.md"), readme)
75
-
76
- console.log("\n✓ Plugin built successfully")
77
- console.log(` Output: ${join(DIST, "plugin.mjs")}`)
78
- console.log(` Package: ${join(DIST, "package.json")}`)
79
- }
80
-
81
- buildPlugin().catch(error => {
82
- console.error(error)
83
- process.exit(1)
84
- })
@@ -1,39 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [Parameter(Mandatory = $true)]
4
- [string] $Destination
5
- )
6
-
7
- $ErrorActionPreference = "Stop"
8
- $version = "2.0.28"
9
- $tarball = "https://registry.npmjs.org/@opencode/cli-windows-x64/-/cli-windows-x64-$version.tgz"
10
- $archiveSha256 = "A5A212125E497726A95D6F9C3CDCFE6F1FA35CA61F3F263E6575D557747FEAE2"
11
- $memberSha256 = "9C59FCD39AA9C611AEDA04711BAB9D668CE3145FA5CDE9868F69C31ADCCFD3E3"
12
- $downloadRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { [IO.Path]::GetTempPath() } else { $env:RUNNER_TEMP }
13
- $archive = Join-Path $downloadRoot "opencode-cli-windows-x64-$version.tgz"
14
-
15
- New-Item -ItemType Directory -Force -Path $Destination | Out-Null
16
- Invoke-WebRequest -Uri $tarball -OutFile $archive
17
- $actualArchive = (Get-FileHash -Algorithm SHA256 -Path $archive).Hash
18
- if ($actualArchive -ne $archiveSha256) {
19
- throw "OPENCODE_ARCHIVE_HASH_MISMATCH"
20
- }
21
-
22
- tar -xzf $archive -C $Destination
23
- $binary = Join-Path $Destination "package\bin\opencode.exe"
24
- if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) {
25
- throw "OPENCODE_BINARY_MISSING"
26
- }
27
- $actualMember = (Get-FileHash -Algorithm SHA256 -Path $binary).Hash
28
- if ($actualMember -ne $memberSha256) {
29
- throw "OPENCODE_BINARY_HASH_MISMATCH"
30
- }
31
- $reported = (& $binary --version 2>&1 | Out-String).Trim()
32
- if ($reported -notmatch [regex]::Escape($version)) {
33
- throw "OPENCODE_VERSION_MISMATCH"
34
- }
35
-
36
- if ($env:GITHUB_ENV) {
37
- "OPENCODE_SMOKE_BINARY=$binary" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
38
- }
39
- Write-Output "Verified OpenCode $version Windows x64 host: $binary"
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- destination="${1:?destination required}"
5
- version="2.0.28"
6
- url="https://registry.npmjs.org/@opencode/cli-linux-x64/-/cli-linux-x64-${version}.tgz"
7
- archive_sha256="76F8505BAAF56965ED28F305B74C526B54ECCD18FE78C81621A4090CC026C9F9"
8
- member_sha256="20D305D0140BFE04DF7264A4ECB014402B74B6AC0A0F6456EE07EF3325FB4A85"
9
- archive="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/opencode-cli-linux-x64-${version}.tgz"
10
-
11
- mkdir -p "$destination"
12
- curl --fail --location --silent --show-error "$url" --output "$archive"
13
- actual_archive="$(sha256sum "$archive" | awk '{print toupper($1)}')"
14
- [[ "$actual_archive" == "$archive_sha256" ]] || { echo "OPENCODE_ARCHIVE_HASH_MISMATCH" >&2; exit 1; }
15
-
16
- tar -xzf "$archive" -C "$destination"
17
- binary="$destination/package/bin/opencode"
18
- [[ -f "$binary" ]] || { echo "OPENCODE_BINARY_MISSING" >&2; exit 1; }
19
- chmod 0755 "$binary"
20
- actual_member="$(sha256sum "$binary" | awk '{print toupper($1)}')"
21
- [[ "$actual_member" == "$member_sha256" ]] || { echo "OPENCODE_BINARY_HASH_MISMATCH" >&2; exit 1; }
22
- "$binary" --version | grep -F "$version" >/dev/null || { echo "OPENCODE_VERSION_MISMATCH" >&2; exit 1; }
23
-
24
- if [[ -n "${GITHUB_ENV:-}" ]]; then
25
- printf 'OPENCODE_SMOKE_BINARY=%s\n' "$binary" >> "$GITHUB_ENV"
26
- fi
27
- echo "Verified OpenCode $version Linux x64 host: $binary"
package/scripts/smoke.mjs DELETED
@@ -1,105 +0,0 @@
1
- import { readFile } from "node:fs/promises"
2
- import { fileURLToPath } from "node:url"
3
- import { p5TransparentRoundtrip } from "../tests/support/smoke-cases.ts"
4
-
5
- // Fail-closed smoke runner. It never discovers or executes a user's OpenCode
6
- // service: only explicitly bound case handlers may launch the sandboxed host.
7
- //
8
- // Exit codes: 0 = every selected implemented case passed, 1 = a case failed,
9
- // 2 = refusal (invalid arguments, unqualified selection, unimplemented case).
10
- // Handlers return a public evidence summary string, throw on failure and are
11
- // responsible for keeping captures, canary scans and run paths private.
12
-
13
- const fail = (code, message) => {
14
- console.error(`${code}: ${message}`)
15
- process.exit(2)
16
- }
17
-
18
- const args = process.argv.slice(2)
19
- let list = false
20
- let qualification = false
21
- let run = null
22
-
23
- for (let index = 0; index < args.length; index += 1) {
24
- const arg = args[index]
25
- if (arg === "--list") {
26
- list = true
27
- } else if (arg === "--qualification") {
28
- qualification = true
29
- } else if (arg === "--run") {
30
- const value = args[index + 1]
31
- if (value === undefined || value.startsWith("--")) {
32
- fail("SMOKE_INVALID_ARGUMENT", "--run requires a case id or 'all'.")
33
- }
34
- run = value
35
- index += 1
36
- } else {
37
- fail("SMOKE_INVALID_ARGUMENT", `unknown argument '${arg}'; use --list, --qualification, or --run <case-id|all>.`)
38
- }
39
- }
40
-
41
- if (args.length === 0) {
42
- fail("SMOKE_INVALID_ARGUMENT", "select --run <case-id|all> to execute; --list shows the inventory.")
43
- }
44
- if (list && (qualification || run !== null)) {
45
- fail("SMOKE_INVALID_ARGUMENT", "--list is inventory-only and cannot be combined with execution modes.")
46
- }
47
-
48
- const repoRoot = fileURLToPath(new URL("..", import.meta.url))
49
- const inventory = JSON.parse(await readFile(new URL("../tests/smoke/cases.json", import.meta.url), "utf8"))
50
- const cases = Array.isArray(inventory?.cases) ? inventory.cases : []
51
-
52
- if (list) {
53
- for (const item of cases) {
54
- console.log(`${item.id} [${item.status}] ${item.profile}/${item.expectation}: ${item.title}`)
55
- }
56
- console.log("No smoke tests executed. No runtime is qualified.")
57
- process.exit(0)
58
- }
59
-
60
- if (qualification) {
61
- fail("SMOKE_NOT_IMPLEMENTED", "qualification gates remain unmet; see tests/smoke/qualification.json.")
62
- }
63
-
64
- // Explicit handler registry: a case may only execute when its inventory entry
65
- // is marked implemented AND it is bound here. Nothing is discovered from the
66
- // filesystem or the environment.
67
- const handlers = new Map([
68
- ["P5-TRANSPARENT-ROUNDTRIP", p5TransparentRoundtrip],
69
- ["P5-SINKS", p5TransparentRoundtrip],
70
- ["P5-BODIES", p5TransparentRoundtrip],
71
- ])
72
-
73
- const select = (id) => {
74
- const entry = cases.find((candidate) => candidate?.id === id)
75
- if (!entry) {
76
- fail("SMOKE_UNKNOWN_CASE", `no case '${id}' in tests/smoke/cases.json.`)
77
- }
78
- if (entry.status !== "implemented" || !handlers.has(id)) {
79
- fail("SMOKE_CASE_NOT_IMPLEMENTED", `case '${id}' is [${entry.status}] with no bound handler.`)
80
- }
81
- return entry
82
- }
83
-
84
- let targets
85
- if (run === "all") {
86
- targets = cases.filter((entry) => entry?.status === "implemented" && handlers.has(entry.id))
87
- if (targets.length === 0) {
88
- fail("SMOKE_NO_IMPLEMENTED_CASES", "no case in tests/smoke/cases.json is both implemented and bound to a handler.")
89
- }
90
- } else {
91
- targets = [select(run)]
92
- }
93
-
94
- let failed = 0
95
- for (const entry of targets) {
96
- console.log(`== ${entry.id}: ${entry.title}`)
97
- try {
98
- const evidence = await handlers.get(entry.id)({ entry, repoRoot })
99
- console.log(`PASS ${entry.id}: ${evidence}`)
100
- } catch (error) {
101
- failed += 1
102
- console.error(`FAIL ${entry.id}: ${error instanceof Error ? error.message : String(error)}`)
103
- }
104
- }
105
- process.exit(failed > 0 ? 1 : 0)
@@ -1,18 +0,0 @@
1
- import { readFile } from "node:fs/promises"
2
- import { spawnSync } from "node:child_process"
3
-
4
- const npm = process.platform === "win32" ? "cmd.exe" : "npm"
5
- const npmArgs = process.platform === "win32"
6
- ? ["/d", "/s", "/c", "npm pack ./dist --dry-run --json --ignore-scripts"]
7
- : ["pack", "./dist", "--dry-run", "--json", "--ignore-scripts"]
8
- const packageJson = JSON.parse(await readFile("dist/package.json", "utf8"))
9
- const result = spawnSync(npm, npmArgs, {
10
- encoding: "utf8",
11
- })
12
- if (result.status !== 0 || result.error) throw new Error("PACKAGE_PACK_FAILED")
13
- const report = JSON.parse(result.stdout)
14
- const files = new Set(report[0]?.files?.map((file) => file.path) ?? [])
15
- const expected = new Set(["LICENSE", "README.md", "package.json", "plugin.mjs"])
16
- if (packageJson.name !== "opencode-secrets-obfuscation" || !/^[0-9]+\.[0-9]+\.[0-9]+/.test(packageJson.version)) throw new Error("PACKAGE_METADATA_INVALID")
17
- if (files.size !== expected.size || [...files].some((file) => !expected.has(file))) throw new Error("PACKAGE_CONTENTS_INVALID")
18
- console.log(`Verified ${packageJson.name}@${packageJson.version}: ${[...files].join(", ")}`)
package/src/README.md DELETED
@@ -1,22 +0,0 @@
1
- # Implementation placeholders
2
-
3
- No plugin entrypoint or implementation exists yet. Do not add a no-op plugin that
4
- could appear active while providing no protection. This directory is not an
5
- auto-discovered `.opencode/plugins` directory.
6
-
7
- | Directory | Future responsibility | First tests |
8
- | --- | --- | --- |
9
- | `config/` | Strict options and admission policy | Unit authorization/config cases |
10
- | `sources/` | Dotenv parsing and opened-handle native adapters | P0 real OS fixtures, then unit parser cases |
11
- | `store/` | Activation/Location/session maps and handle lifecycle | Unit store and contract lifecycle cases |
12
- | `sanitize/` | Admissibility, overlap unions, exact sink projections | Unit oracles, then real-host spill smoke |
13
- | `policy/` | Enrollment and per-call authorization | Unit denials, then host-policy gap smoke |
14
- | `budgets/` | Reservations and aggregate ownership | Unit budget and multi-activation smoke |
15
- | `tools/` | Reviewed access check and sanitized source view | Integration fixture, then host round trip |
16
- | `opencode/v2/` | Effect entrypoint, hooks and lifecycle | P0 probe plugins and real-host contracts |
17
- | `diagnostics/` | Safe codes and coverage/readiness reporting | No-value diagnostics tests |
18
-
19
- Add runtime dependencies only after the P0 release/Effect compatibility spike.
20
- The development TypeScript/Node versions do not choose an OpenCode runtime.
21
-
22
- See [design](../docs/spec/design.md) and [test strategy](../docs/testing.md).
@@ -1,49 +0,0 @@
1
- export type BudgetDimension = "sourceCount" | "sourceBytes" | "totalSourceBytes" | "secretCount" | "secretBytes" | "indexBytes" | "inputBytes" | "outputBytes" | "traversalDepth" | "traversalNodes" | "activeSessions" | "handlesPerSession" | "activeCalls" | "workingBytes" | "calls" | "nodes" | "depth" | "sessions" | "handles"
2
- export type BudgetValues = Partial<Record<BudgetDimension, number>>
3
- export type BudgetResult = { readonly ok: true; readonly value: { dispose(): void } } | { readonly ok: false }
4
-
5
- const dimensions = new Set<string>(["sourceCount", "sourceBytes", "totalSourceBytes", "secretCount", "secretBytes", "indexBytes", "inputBytes", "outputBytes", "traversalDepth", "traversalNodes", "activeSessions", "handlesPerSession", "activeCalls", "workingBytes", "calls", "nodes", "depth", "sessions", "handles"])
6
- export class BudgetLedger {
7
- private readonly used = new Map<string, number>()
8
- private readonly limits: BudgetValues
9
- constructor(limits: BudgetValues) {
10
- const values = inspect(limits)
11
- if (!values) throw new Error("invalid")
12
- for (const [key, value] of values) if (!dimensions.has(key) || typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("invalid")
13
- this.limits = Object.fromEntries(values) as BudgetValues
14
- }
15
- reserve(request: BudgetValues): BudgetResult {
16
- try {
17
- const values = inspect(request)
18
- if (!values) return { ok: false }
19
- for (const [key, value] of values) {
20
- if (!dimensions.has(key) || typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return { ok: false }
21
- const limit = this.limits[key as BudgetDimension]
22
- const current = this.used.get(key) ?? 0
23
- if (value > Number.MAX_SAFE_INTEGER - current) return { ok: false }
24
- if (limit !== undefined && current + value > limit) return { ok: false }
25
- }
26
- const committed = values as readonly (readonly [string, number])[]
27
- for (const [key, value] of committed) this.used.set(key, (this.used.get(key) ?? 0) + value)
28
- let disposed = false
29
- return { ok: true, value: { dispose: () => { if (disposed) return; disposed = true; for (const [key, value] of committed) this.used.set(key, (this.used.get(key) ?? 0) - value) } } }
30
- } catch { return { ok: false } }
31
- }
32
- }
33
-
34
- function inspect(value: unknown): readonly (readonly [string, unknown])[] | undefined {
35
- try {
36
- if (!value || typeof value !== "object") return undefined
37
- if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return undefined
38
- const keys = Reflect.ownKeys(value)
39
- if (keys.some(key => typeof key !== "string")) return undefined
40
- const result: [string, unknown][] = []
41
- for (const key of keys) {
42
- if (typeof key !== "string") return undefined
43
- const descriptor = Object.getOwnPropertyDescriptor(value, key)
44
- if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return undefined
45
- result.push([key, descriptor.value])
46
- }
47
- return result
48
- } catch { return undefined }
49
- }
@@ -1,4 +0,0 @@
1
- # Placeholder: configuration
2
-
3
- Strict schema, required authorization mode, explicit enrollment, reviewed operation
4
- IDs and hard resource ceilings. No implementation yet.