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
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.
|
package/scripts/build-plugin.mjs
DELETED
|
@@ -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).
|
package/src/budgets/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/config/README.md
DELETED