opencode-thrum-pro 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.
- package/assets/commands/thrum-_stamp-protocol.md +65 -0
- package/assets/skills/brainstorm-steward/SKILL.md +15 -0
- package/assets/skills/choosing-subagent-models/SKILL.md +19 -11
- package/assets/skills/conversation-only-context/SKILL.md +186 -0
- package/assets/skills/coordinator-dispatching-work/SKILL.md +28 -8
- package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +68 -0
- package/assets/skills/coordinator-merging-code/SKILL.md +24 -0
- package/assets/skills/coordinator-running-brainstorm-cycles/SKILL.md +11 -7
- package/assets/skills/efficient-multi-agent-research/SKILL.md +7 -6
- package/assets/skills/persistent-watcher-archetype/SKILL.md +68 -9
- package/assets/skills/persistent-watcher-archetype/resources/thrum-capture-fallback.sh +123 -0
- package/assets/skills/persistent-watcher-archetype/resources/thrum-watch-pane-capture.sh +379 -0
- package/assets/skills/prime-agent/SKILL.md +2 -4
- package/assets/skills/project-setup/SKILL.md +118 -21
- package/assets/skills/update-queue/SKILL.md +3 -1
- package/assets/thrum_allowlist.json +34 -0
- package/dist/allowlist-sync.test.d.ts +1 -0
- package/dist/allowlist-sync.test.js +44 -0
- package/dist/index.js +24 -1
- package/dist/installer.d.ts +29 -0
- package/dist/installer.js +356 -1
- package/dist/installer.permissions.test.d.ts +1 -0
- package/dist/installer.permissions.test.js +602 -0
- package/dist/permission-fixture.integration.test.d.ts +1 -0
- package/dist/permission-fixture.integration.test.js +506 -0
- package/package.json +3 -2
|
@@ -44,7 +44,45 @@ grep -E 'verify=Ready:Yes([[:space:]]|-->|$)' "$PLAN_FILE" \
|
|
|
44
44
|
|| grep -E 'verify=PREDATES([[:space:]]|-->|$)' "$PLAN_FILE"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
**Fourth check — the stamp must BIND, not just be PRESENT.** A stamp's
|
|
48
|
+
presence proves a review happened; it does not prove the stamp still
|
|
49
|
+
describes the artifact in front of you. Parse the binding keys from the
|
|
50
|
+
**highest-cycle** (last-appended) `stage=plan` line — stamps are only ever
|
|
51
|
+
appended, never reordered, so `tail -1` is the binding verdict; never `sort`
|
|
52
|
+
(a `cycle=10` line must beat `cycle=9`, which a lexical sort inverts) — then
|
|
53
|
+
re-derive the current values per `_stamp-protocol.md` § "Review-object
|
|
54
|
+
binding" and compare:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# SOURCE_FILE = the LOCKED brainstorm (+ design spec, if any) this plan was reviewed against
|
|
58
|
+
LINE=$(grep '^<!-- THRUM-REVIEW: stage=plan ' "$PLAN_FILE" | tail -1)
|
|
59
|
+
|
|
60
|
+
# An OVERRIDE on this line already satisfies the fourth check (same escape as
|
|
61
|
+
# the first check) — short-circuit BEFORE the missing-keys exit below, or a
|
|
62
|
+
# legitimate OVERRIDE stamp that omits plan_obj/src_lock is wrongly rejected.
|
|
63
|
+
if ! grep -qE 'verdict=OVERRIDE([[:space:]]|-->|$)' <<<"$LINE"; then
|
|
64
|
+
STAMPED_OBJ=$(sed -n 's/.*plan_obj=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
|
|
65
|
+
STAMPED_LOCK=$(sed -n 's/.*src_lock=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
|
|
66
|
+
|
|
67
|
+
# Missing keys fails exactly like a missing verify= — no silent-omission path.
|
|
68
|
+
[ -n "$STAMPED_OBJ" ] && [ -n "$STAMPED_LOCK" ] || {
|
|
69
|
+
echo "review stamp missing plan_obj/src_lock binding keys" >&2; exit 1; }
|
|
70
|
+
|
|
71
|
+
CURRENT_OBJ=$(grep -v '^<!-- THRUM-REVIEW:' "$PLAN_FILE" | git hash-object --no-filters --stdin)
|
|
72
|
+
CURRENT_LOCK=$(git hash-object --no-filters "$SOURCE_FILE")
|
|
73
|
+
|
|
74
|
+
[ "$STAMPED_OBJ" = "$CURRENT_OBJ" ] \
|
|
75
|
+
|| { echo "review stamp does not bind the current plan object" >&2; exit 1; }
|
|
76
|
+
[ "$STAMPED_LOCK" = "$CURRENT_LOCK" ] \
|
|
77
|
+
|| { echo "review stamp bound to a superseded source LOCK" >&2; exit 1; }
|
|
78
|
+
fi
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
An OVERRIDE on the matched `stage=plan` line already satisfies this fourth
|
|
82
|
+
check (same escape as the first check) — the snippet above short-circuits
|
|
83
|
+
before re-deriving or comparing in that case.
|
|
84
|
+
|
|
85
|
+
All four checks must pass.
|
|
48
86
|
|
|
49
87
|
Notes on the check:
|
|
50
88
|
|
|
@@ -75,29 +113,42 @@ Notes on the check:
|
|
|
75
113
|
`OVERRIDE`. This is what makes `verify-against-source` mandatory in fact,
|
|
76
114
|
not just in name: a plan could otherwise carry `verdict=Ready:Yes` from
|
|
77
115
|
some OTHER reviewer while `verify-against-source` itself never ran.
|
|
116
|
+
- **The binding check fails closed on either mismatch, with no
|
|
117
|
+
silent-omission path** — a stamp missing `plan_obj`/`src_lock` entirely
|
|
118
|
+
fails exactly like a missing `verify=` field (fourth check above). The
|
|
119
|
+
remedy is the same shape as the `verify=` remedy: re-run the plan review so
|
|
120
|
+
the stamp is emitted with live binding keys (`_stamp-protocol.md` § "Review-
|
|
121
|
+
object binding"), or a coordinator adds an explicit `verdict=OVERRIDE`
|
|
122
|
+
(with `reason=`) to the `stage=plan` line — the SAME auditable escape used
|
|
123
|
+
for the first check, not a new mechanism. There is no `PREDATES` value for
|
|
124
|
+
the binding keys themselves; a legacy plan without them either gets
|
|
125
|
+
re-reviewed (which produces the keys) or overridden.
|
|
78
126
|
|
|
79
127
|
If any check fails, **STOP — do not proceed.** Tell the user:
|
|
80
128
|
|
|
81
129
|
> project-setup requires the plan/spec to have passed the planning-loop review
|
|
82
130
|
> (or carry a coordinator override), to carry a `## Deviations from Source`
|
|
83
|
-
> block,
|
|
84
|
-
>
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
>
|
|
88
|
-
>
|
|
89
|
-
>
|
|
90
|
-
> `verify
|
|
91
|
-
>
|
|
131
|
+
> block, to carry `verify=Ready:Yes`, `verify=OVERRIDE`, or `verify=PREDATES`
|
|
132
|
+
> evidence that `verify-against-source` specifically ran (or was deliberately
|
|
133
|
+
> waived, or the plan predates the convention), AND for the review stamp to
|
|
134
|
+
> BIND the current plan object and current locked source (`plan_obj`/
|
|
135
|
+
> `src_lock`). The plan at `<PLAN_FILE>` is missing one or more of: the review
|
|
136
|
+
> stamp, the Deviations block, the `verify=` field, or a valid binding — check
|
|
137
|
+
> which with the commands above. Run the plan review first (making sure
|
|
138
|
+
> `verify-against-source` runs, `verify=` is stamped, and `plan_obj`/
|
|
139
|
+
> `src_lock` are computed fresh), obtain a coordinator override, or stamp
|
|
140
|
+
> `verify=PREDATES` if the plan predates the `verify=` convention, then
|
|
141
|
+
> re-invoke project-setup.
|
|
92
142
|
|
|
93
143
|
**Fail closed, not open.** Plans that predate this feature, or that arrive from
|
|
94
|
-
a different flow, will not carry the stamp, the Deviations block,
|
|
95
|
-
`verify=` field — they still bail on all
|
|
96
|
-
`OVERRIDE` stamp for the review-stamp check
|
|
144
|
+
a different flow, will not carry the stamp, the Deviations block, the
|
|
145
|
+
`verify=` field, or the binding keys — they still bail on all four checks. The
|
|
146
|
+
caller must add an `OVERRIDE` stamp for the review-stamp check (which also
|
|
147
|
+
covers the binding check, per the note above) and a `verify=PREDATES` (the
|
|
97
148
|
legacy case — not `verify=OVERRIDE`, which is reserved for an active per-cycle
|
|
98
149
|
waiver) for the verify check, and must author the Deviations block (there is
|
|
99
150
|
no override for its absence), to proceed. Do not silently fall through when
|
|
100
|
-
any of the
|
|
151
|
+
any of the four is absent.
|
|
101
152
|
|
|
102
153
|
## When to Use
|
|
103
154
|
|
|
@@ -901,11 +952,13 @@ prompt file:
|
|
|
901
952
|
<!-- THRUM-GATE: stage=prompt next=dispatch -->
|
|
902
953
|
```
|
|
903
954
|
|
|
904
|
-
2. **After the post-setup prompt review terminates `Ready:Yes`**,
|
|
905
|
-
|
|
955
|
+
2. **After the post-setup prompt review terminates `Ready:Yes`**, compute
|
|
956
|
+
`prompt_obj`/`plan_lock` per `_stamp-protocol.md` § "Review-object binding"
|
|
957
|
+
(strip-then-hash the prompt against itself for `prompt_obj`; plain-hash the
|
|
958
|
+
LOCKED plan for `plan_lock`), then append the verdict stamp:
|
|
906
959
|
|
|
907
960
|
```text
|
|
908
|
-
<!-- THRUM-REVIEW: stage=prompt verdict=Ready:Yes cycle=<N> date=<YYYY-MM-DD> -->
|
|
961
|
+
<!-- THRUM-REVIEW: stage=prompt verdict=Ready:Yes cycle=<N> date=<YYYY-MM-DD> prompt_obj=<blob> plan_lock=<blob> -->
|
|
909
962
|
```
|
|
910
963
|
|
|
911
964
|
This review happens **within the same project-setup session**: after Step 4
|
|
@@ -916,10 +969,54 @@ prompt file:
|
|
|
916
969
|
|
|
917
970
|
Use the exact canonical, case-sensitive form (`Ready:Yes` / `OVERRIDE`; keep
|
|
918
971
|
keys in the fixed order `stage=`, `verdict=`, `cycle=`, `date=` so a literal
|
|
919
|
-
`grep -F` matches).
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
972
|
+
`grep -F` matches).
|
|
973
|
+
|
|
974
|
+
**This stamp is a hard completion requirement, not an optional shortcut.**
|
|
975
|
+
project-setup CANNOT report completion or dispatch-readiness for this session
|
|
976
|
+
until the generated prompt carries a `stage=prompt verdict=Ready:Yes` (or
|
|
977
|
+
`OVERRIDE`) THRUM-REVIEW line with valid `prompt_obj`/`plan_lock` bindings.
|
|
978
|
+
Run the same binding check used at Phase 0 (§ "Fourth check" above), against
|
|
979
|
+
the prompt instead of the plan:
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
# PROMPT_FILE = the generated prompt; PLAN_FILE = the LOCKED plan it was reviewed against
|
|
983
|
+
LINE=$(grep '^<!-- THRUM-REVIEW: stage=prompt ' "$PROMPT_FILE" | tail -1)
|
|
984
|
+
[ -n "$LINE" ] || {
|
|
985
|
+
echo "prompt carries no stage=prompt review stamp — cannot report dispatch-ready" >&2
|
|
986
|
+
exit 1
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
# An OVERRIDE on this line already satisfies the check (same escape as
|
|
990
|
+
# Phase 0's fourth check) — short-circuit BEFORE the missing-keys exit below,
|
|
991
|
+
# or a legitimate OVERRIDE stamp that omits prompt_obj/plan_lock is wrongly
|
|
992
|
+
# rejected.
|
|
993
|
+
if ! grep -qE 'verdict=OVERRIDE([[:space:]]|-->|$)' <<<"$LINE"; then
|
|
994
|
+
STAMPED_OBJ=$(sed -n 's/.*prompt_obj=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
|
|
995
|
+
STAMPED_LOCK=$(sed -n 's/.*plan_lock=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
|
|
996
|
+
[ -n "$STAMPED_OBJ" ] && [ -n "$STAMPED_LOCK" ] || {
|
|
997
|
+
echo "review stamp missing prompt_obj/plan_lock binding keys" >&2; exit 1; }
|
|
998
|
+
|
|
999
|
+
CURRENT_OBJ=$(grep -v '^<!-- THRUM-REVIEW:' "$PROMPT_FILE" | git hash-object --no-filters --stdin)
|
|
1000
|
+
CURRENT_LOCK=$(git hash-object --no-filters "$PLAN_FILE")
|
|
1001
|
+
|
|
1002
|
+
[ "$STAMPED_OBJ" = "$CURRENT_OBJ" ] \
|
|
1003
|
+
|| { echo "review stamp does not bind the current prompt object" >&2; exit 1; }
|
|
1004
|
+
[ "$STAMPED_LOCK" = "$CURRENT_LOCK" ] \
|
|
1005
|
+
|| { echo "review stamp bound to a superseded plan LOCK" >&2; exit 1; }
|
|
1006
|
+
fi
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
An OVERRIDE on the matched line already satisfies this check (same escape as
|
|
1010
|
+
Phase 0's fourth check) — the snippet above short-circuits before re-deriving
|
|
1011
|
+
or comparing in that case.
|
|
1012
|
+
|
|
1013
|
+
At dispatch, the coordinator greps the prompt for this stamp. On a
|
|
1014
|
+
project-setup-produced prompt it is ALWAYS present — project-setup will not
|
|
1015
|
+
have reported completion without it — so its absence on such a prompt is an
|
|
1016
|
+
ANOMALY requiring the full pre-dispatch review, never a sanctioned shortcut
|
|
1017
|
+
for skipping it. (Pre-feature prompts and prompts from non-project-setup
|
|
1018
|
+
flows are a separate, narrower carve-out — see
|
|
1019
|
+
`coordinator-dispatching-work`.)
|
|
923
1020
|
|
|
924
1021
|
## Common Mistakes
|
|
925
1022
|
|
|
@@ -112,7 +112,9 @@ Give each sub-agent:
|
|
|
112
112
|
`merge-base`, `status` ONLY — never `checkout`/`reset`/`restore`/`stash`/
|
|
113
113
|
`clean`/`rebase`/`commit`/`push`, in any directory; never enter another
|
|
114
114
|
worktree.
|
|
115
|
-
- pinned `model:`
|
|
115
|
+
- pinned `model:` (sonnet; intended tier is low for mechanical lookups — the
|
|
116
|
+
Agent tool takes no `effort`, so carry the tier via the agent definition or
|
|
117
|
+
the tmux launch flags).
|
|
116
118
|
|
|
117
119
|
## Step 5 — Add missing committed work
|
|
118
120
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_supersedes": "the earlier watcher-settings allowlist spec (single-watcher restart-flow scope)",
|
|
4
|
+
"_comment": [
|
|
5
|
+
"This is the canonical, code-consumed source for the Thrum ordinary-operation allowlist (allowlist).",
|
|
6
|
+
"TWO-CAVEAT SPLIT (owner ruling) — read both before editing this file:",
|
|
7
|
+
" 1. SUPERSEDED: the original spec's 'scoped to one watcher's restart flow, not a general starting point' framing is superseded. This IS the general, code-consumed source every runtime generator draws from.",
|
|
8
|
+
" 2. STILL BINDING, NOT SUPERSEDED — BUT THE MODEL CHANGED (owner ruling via the fleet coordinator, allowlist consolidated round): command_patterns is no longer an enumerated per-subcommand list. The owner ruled ALL thrum commands are globally allowlisted — 'thrum' itself is the ONE entry, representing the whole command FAMILY (any subcommand). This is still bounded, never a blanket Bash(*): the ONLY thing that admits a candidate is an exact executable-token match (bare 'thrum', or the Part-C canonical-install-path forms) at an executable boundary, and the fail-closed metachar/alt-exec guard (containsForbiddenFragment + the alt-exec check in matchesCommandPattern) is now the PRIMARY scoping boundary, not a secondary one — it alone is what stops sh -c/bash -c, command substitution ($(...)/backticks), compound commands (&&, ;, |, ||), and alternate executable paths (./thrum, /tmp/thrum, arbitrary absolute paths, suffix/prefix lookalikes like thrum-evil). Every generator consuming this file must render the FAMILY grant in its native syntax (Claude/OpenCode: any-subcommand pattern, no shell composition; Codex: native profile only, §5.1, no invented Bash allowlist — command_patterns is not a Codex concept) and prove the negative controls above still hold — see each generator's test file.",
|
|
9
|
+
"READ-PATH INVARIANT: read_paths below (including the recursive '.thrum/**'-shaped grant every generator renders from '.thrum') MUST NEVER be allowed to carry secrets. .thrum is granted broad READ access across every supported runtime by this file — if any future change adds credentials, tokens, or private keys under .thrum, this allowlist makes them world-readable to every runtime this file feeds. Keep secrets out of .thrum, not out of this file.",
|
|
10
|
+
"GLOBAL READ PATHS (allowlist round-4, owner P0): global_read_paths grants ABSOLUTE, home-relative directories outside the project root — distinct from read_paths above, which are project-relative. Per-runtime because the underlying installed-plugin-cache layout differs by runtime (no shared portable form exists): claude = ~/.claude/plugins/cache/thrum (the thrum marketplace/plugin's OWN cache subtree, version-hash-stripped — NEVER pin a specific installed <version> segment, and never grant the broader ~/.claude/plugins/cache root, which also holds OTHER installed marketplaces' plugin payload); codex = ~/.codex/skills (Codex's flattened global skill install dir — granted in full because Codex's permission syntax has no glob/prefix-match primitive to scope it to thrum-named skills only; it also exposes non-thrum skill dirs as an accepted, documented trade-off) + ~/.codex/plugins/cache/thrum-marketplace/thrum (the versioned plugin-cache mirror, same version-hash-stripping rule as Claude's). OpenCode needs NO entry here — its own installer (opencode-plugin/src/installer.ts, opencodeGlobalReadPaths) already derives its global skill/command paths dynamically from its own resolved configDir at generation time, so a static path here would be redundant and could drift from the real value.",
|
|
11
|
+
"OWNER-AUTHORIZED EXCEPTION — /private/tmp (owner ruling, watcher-recovery P0; scope CONFIRMED all-runtimes by the fleet coordinator the same day — owner's wording was 'global configs for all agents', the operational-artifact class is runtime-independent): owner_authorized_exceptions grants EVERY supported runtime broad READ access to /private/tmp/* DESPITE it being a world-writable directory — explicitly ruled acceptable by the owner for coordinator/watcher operational artifacts. This is a DELIBERATE exception, not a template for widening elsewhere: it grants READ/ACCESS ONLY — never execute, never shell-interpolation, never a sibling root like /private or /private/tmpfoo, never write/delete. Codex renders this via its NATIVE filesystem-read profile mechanism, never as an invented Bash-pattern entry (still-binding caveat above). Every generator consuming this list must prove those boundaries by negative-control test AND by-effect (a real read actually succeeds; a real write/exec attempt actually fails), not merely by the shape of the grant string.",
|
|
12
|
+
"~/.local/bin OWNER CORRECTION (owner ruling, consolidated round): global_read_paths.<runtime> gains '~/.local/bin' for ALL THREE runtimes — the install location of the thrum binary itself, so PATH-resolved 'thrum' can actually execute under each runtime's sandbox. Granted in full (not scoped to just the thrum binary file) for the same reason as Codex's flattened ~/.codex/skills grant above: none of the three permission syntaxes has a single-file-only primitive that stays portable, so the directory is the smallest expressible unit. This is a FILESYSTEM READ/ACCESS/EXEC grant on the install directory — it is a SEPARATE axis from command_patterns matching, and does NOT loosen the alt-exec guard in approved.go beyond the tight, explicitly-scoped canonical-install-path carve-out documented in that file (canonicalInstallPathPrefixes) — a lookalike executable in the same directory (~/.local/bin/thrum-evil) is still rejected by that carve-out's own boundary check even though the DIRECTORY grant here would let a runtime read/access it."
|
|
13
|
+
],
|
|
14
|
+
"command_patterns": [
|
|
15
|
+
"thrum"
|
|
16
|
+
],
|
|
17
|
+
"read_paths": [
|
|
18
|
+
".claude/skills",
|
|
19
|
+
".claude/commands",
|
|
20
|
+
".codex/skills",
|
|
21
|
+
".opencode/skills",
|
|
22
|
+
".thrum"
|
|
23
|
+
],
|
|
24
|
+
"global_read_paths": {
|
|
25
|
+
"claude": ["~/.claude/plugins/cache/thrum", "~/.local/bin"],
|
|
26
|
+
"codex": ["~/.codex/skills", "~/.codex/plugins/cache/thrum-marketplace/thrum", "~/.local/bin"],
|
|
27
|
+
"opencode": ["~/.local/bin"]
|
|
28
|
+
},
|
|
29
|
+
"owner_authorized_exceptions": {
|
|
30
|
+
"claude": ["/private/tmp"],
|
|
31
|
+
"codex": ["/private/tmp"],
|
|
32
|
+
"opencode": ["/private/tmp"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Drift guard (allowlist round 2, finding 9): the canonical allowlist
|
|
2
|
+
// (internal/permissions/thrum_allowlist.json, Go-owned) is copied verbatim
|
|
3
|
+
// into this plugin's bundled asset (opencode-plugin/assets/thrum_allowlist.
|
|
4
|
+
// json) by scripts/sync-skills.sh's sync_opencode step, since TypeScript
|
|
5
|
+
// cannot import a Go package. This test fails LOUDLY the moment the two
|
|
6
|
+
// diverge — a hand-edit of either copy, or a Go-side change that never got
|
|
7
|
+
// synced.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately compares RAW BYTES, not parsed-then-reserialized JSON: a
|
|
10
|
+
// round-trip through JSON.parse/JSON.stringify would normalize whitespace,
|
|
11
|
+
// key order, and formatting, and could mask a real drift (e.g. a
|
|
12
|
+
// re-ordered or re-indented — but content-different — copy would compare
|
|
13
|
+
// "equal" after reserialization while still being a byte-for-byte
|
|
14
|
+
// divergence from the Go source of truth).
|
|
15
|
+
import { test } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
// This test file compiles to dist/allowlist-sync.test.js (flat src ->
|
|
20
|
+
// flat dist, see tsconfig rootDir/outDir), so at runtime
|
|
21
|
+
// path.dirname(import.meta.url) is opencode-plugin/dist. One level up is
|
|
22
|
+
// opencode-plugin/ (matches the existing ALLOWLIST_ASSET_PATH convention in
|
|
23
|
+
// installer.permissions.test.ts); two levels up is the repo root, from
|
|
24
|
+
// which internal/permissions/ is reachable.
|
|
25
|
+
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
26
|
+
const PLUGIN_ROOT = path.resolve(HERE, "..");
|
|
27
|
+
const REPO_ROOT = path.resolve(PLUGIN_ROOT, "..");
|
|
28
|
+
const OPENCODE_COPY = path.join(PLUGIN_ROOT, "assets", "thrum_allowlist.json");
|
|
29
|
+
const CANONICAL_SOURCE = path.join(REPO_ROOT, "internal", "permissions", "thrum_allowlist.json");
|
|
30
|
+
test("opencode-plugin/assets/thrum_allowlist.json is byte-identical to internal/permissions/thrum_allowlist.json", (t) => {
|
|
31
|
+
// The canonical source only exists in the monorepo (github.com/leonletto/thrum);
|
|
32
|
+
// the public standalone opencode-thrum-pro repo this test also ships in does not
|
|
33
|
+
// carry internal/permissions/, so skip gracefully there instead of failing —
|
|
34
|
+
// mirrors the binary-absent skip idiom in permission-fixture.integration.test.ts.
|
|
35
|
+
if (!fs.existsSync(CANONICAL_SOURCE)) {
|
|
36
|
+
t.skip(`canonical source not found at ${CANONICAL_SOURCE} — expected outside the monorepo (e.g. public opencode-thrum-pro), skipping drift check`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
assert.ok(fs.existsSync(OPENCODE_COPY), `synced copy not found at ${OPENCODE_COPY}`);
|
|
40
|
+
const canonical = fs.readFileSync(CANONICAL_SOURCE);
|
|
41
|
+
const copy = fs.readFileSync(OPENCODE_COPY);
|
|
42
|
+
assert.ok(canonical.equals(copy), "opencode-plugin/assets/thrum_allowlist.json has drifted from internal/permissions/thrum_allowlist.json — " +
|
|
43
|
+
"re-run scripts/sync-skills.sh (sync_opencode step) to resync, never hand-edit either copy independently");
|
|
44
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installAssets } from "./installer.js";
|
|
1
|
+
import { installAssets, installPermissions } from "./installer.js";
|
|
2
2
|
import { shellEnvHook } from "./hooks/shell-env.js";
|
|
3
3
|
import { blockSyncCdHook } from "./hooks/block-sync-cd.js";
|
|
4
4
|
const TOOL_MAPPING = `
|
|
@@ -13,6 +13,29 @@ When Thrum skills reference Claude Code tool names, use these OpenCode equivalen
|
|
|
13
13
|
`.trim();
|
|
14
14
|
const server = async (ctx) => {
|
|
15
15
|
await installAssets(ctx);
|
|
16
|
+
// Runs unconditionally (not version-gated like installAssets above): it's
|
|
17
|
+
// cheap and idempotent, and gating it on the skills-version marker would
|
|
18
|
+
// skip re-merging the allowlist for users who already had the plugin
|
|
19
|
+
// installed before this permission block existed (allowlist).
|
|
20
|
+
//
|
|
21
|
+
// installPermissions() throws when the user's existing global
|
|
22
|
+
// opencode.json is present but malformed (unreadable or invalid JSON) —
|
|
23
|
+
// deliberately, so we never silently overwrite it (allowlist landing-
|
|
24
|
+
// blocker). Catch and log here rather than letting it become an unhandled
|
|
25
|
+
// rejection that would abort plugin bootstrap entirely; every other hook
|
|
26
|
+
// this plugin provides should still register.
|
|
27
|
+
try {
|
|
28
|
+
await installPermissions();
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
await ctx.client.app.log({
|
|
32
|
+
body: {
|
|
33
|
+
service: "opencode-thrum",
|
|
34
|
+
level: "error",
|
|
35
|
+
message: `failed to install thrum permission allowlist (leaving your opencode.json untouched): ${err.message}`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
16
39
|
return {
|
|
17
40
|
"shell.env": shellEnvHook(ctx),
|
|
18
41
|
"tool.execute.before": blockSyncCdHook(),
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
1
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
export declare function getConfigDir(): string;
|
|
2
3
|
export declare function installAssets(ctx: PluginInput): Promise<void>;
|
|
4
|
+
interface AllowlistSource {
|
|
5
|
+
version: number;
|
|
6
|
+
command_patterns: string[];
|
|
7
|
+
read_paths: string[];
|
|
8
|
+
owner_authorized_exceptions?: Record<string, string[]>;
|
|
9
|
+
global_read_paths?: Record<string, string[]>;
|
|
10
|
+
}
|
|
11
|
+
type PermissionAction = "ask" | "allow" | "deny";
|
|
12
|
+
type PermissionRuleMap = Record<string, PermissionAction>;
|
|
13
|
+
interface OpenCodeConfig {
|
|
14
|
+
permission?: {
|
|
15
|
+
bash?: PermissionRuleMap | PermissionAction;
|
|
16
|
+
read?: PermissionRuleMap | PermissionAction;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare function bashExactPatternFor(commandPattern: string): string;
|
|
22
|
+
export declare function bashPatternFor(commandPattern: string): string;
|
|
23
|
+
export declare function readPatternFor(readPath: string): string;
|
|
24
|
+
export declare function resolveHomePath(p: string, homeDir?: string): string;
|
|
25
|
+
export declare function readFilePatternFor(filePath: string): string;
|
|
26
|
+
export declare function loadAllowlist(assetPath?: string): AllowlistSource;
|
|
27
|
+
export declare function mergePermissions(config: OpenCodeConfig, allowlist: AllowlistSource, extraReadPatterns?: string[], homeDir?: string): OpenCodeConfig;
|
|
28
|
+
export declare function writeConfig(configPath: string, config: OpenCodeConfig): void;
|
|
29
|
+
export declare function opencodeGlobalReadPaths(configDir: string): string[];
|
|
30
|
+
export declare function installPermissions(configDir?: string, homeDir?: string): Promise<void>;
|
|
31
|
+
export {};
|