pantheon-opencode 1.6.0-beta.7 → 1.6.0-beta.9
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/AGENTS.md +58 -15
- package/CHANGELOG.md +61 -94
- package/README.md +81 -103
- package/README.pt-BR.md +19 -37
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-focus.md +17 -0
- package/package.json +1 -2
- package/plugin.json +2 -2
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +2 -3
- package/scripts/doctor.mjs +22 -104
- package/scripts/hooks/scan-secrets.sh +97 -0
- package/scripts/install/config-migration.mjs +9 -42
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode.mjs +174 -77
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/strings.mjs +0 -8
- package/scripts/probe-pantheon-cost.mjs +3 -16
- package/scripts/sync-artifacts.mjs +1 -0
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +203 -786
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +4 -5
- package/src/agents/aphrodite.md +2 -9
- package/src/agents/demeter.md +0 -9
- package/src/agents/gaia.md +1 -0
- package/src/agents/hephaestus.md +0 -9
- package/src/agents/hermes.md +2 -9
- package/src/agents/mnemosyne.md +18 -53
- package/src/agents/prometheus.md +0 -9
- package/src/agents/themis.md +0 -1
- package/src/agents/zeus.md +180 -19
- package/src/instructions/agent-return-format.instructions.md +48 -3
- package/src/instructions/memory-protocol.instructions.md +8 -9
- package/src/instructions/zeus-anti-stall.instructions.md +54 -6
- package/src/instructions/zeus-council-synthesis.instructions.md +71 -0
- package/src/instructions/zeus-memory-operations.instructions.md +4 -4
- package/src/instructions/zeus-timeout-retry.instructions.md +81 -7
- package/src/pantheon/v2-delegation-enforce.ts +2 -1
- package/src/plugin-v2.ts +251 -168
- package/src/plugins/pantheon-hooks.ts +35 -82
- package/src/plugins/tui/dist/tui-v2.d.ts +9 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +151 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +29 -53
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5411 -1292
- package/src/plugins/tui/package.json +10 -7
- package/src/plugins/tui/src/index-v2.tsx +108 -0
- package/src/plugins/tui/src/index.tsx +28 -63
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsdown.config.ts +2 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +4 -2
- package/src/skills/README.md +7 -12
- package/src/skills/agent-coordination/SKILL.md +83 -13
- package/src/skills/auto-continue/SKILL.md +13 -14
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/pantheon/secret-scanner.ts +0 -489
- package/src/plugins/tui/README.md +0 -14
- package/src/skills/council-synthesis/SKILL.md +0 -29
package/README.pt-BR.md
CHANGED
|
@@ -53,19 +53,6 @@ Com o OpenCode em execução, descreva o resultado que você quer:
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
O Pantheon ajuda a transformar esse pedido em um plano e em etapas revisadas.
|
|
56
|
-
Perguntas sem mudança no repositório são respondidas diretamente; correções
|
|
57
|
-
pequenas usam uma única delegação. Discovery, planejamento, paralelismo e
|
|
58
|
-
revisão em council ficam para quando o escopo ou o risco justificarem.
|
|
59
|
-
|
|
60
|
-
Para uma correção pequena e bem delimitada, chame `@talos` diretamente (por
|
|
61
|
-
exemplo, `@talos corrija este typo`). Assim você pula a chamada de orquestração
|
|
62
|
-
do Zeus; use `/pantheon` para planejamento, delegação, mudanças em vários
|
|
63
|
-
arquivos ou gates de qualidade.
|
|
64
|
-
|
|
65
|
-
O procedimento completo de council só é carregado em `/pantheon` ou quando uma
|
|
66
|
-
decisão material realmente exige perspectivas distintas. Falhas de dispatch
|
|
67
|
-
podem ser tentadas novamente uma vez; testes reprovados e recusas não são
|
|
68
|
-
repetidos automaticamente.
|
|
69
56
|
|
|
70
57
|
## Para quem é?
|
|
71
58
|
|
|
@@ -84,13 +71,11 @@ verificações repetíveis e passagens claras entre etapas do trabalho.
|
|
|
84
71
|
|
|
85
72
|
## Status
|
|
86
73
|
|
|
87
|
-
Versão
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
[releases](https://github.com/ils15/pantheon-opencode/releases) e o
|
|
93
|
-
[changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
|
|
74
|
+
Versão operacional neste checkout: **v1.6.0-beta.1** (primeira beta compatível
|
|
75
|
+
com OpenCode 2; esta página não afirma publicação). O Pantheon foi feito para OpenCode e depende da
|
|
76
|
+
disponibilidade e da configuração do OpenCode e dos serviços opcionais que você
|
|
77
|
+
escolher. Veja as [releases](https://github.com/ils15/pantheon-opencode/releases)
|
|
78
|
+
e o [changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
|
|
94
79
|
|
|
95
80
|
|
|
96
81
|
## Execução code-mode (opt-in explícito)
|
|
@@ -136,13 +121,13 @@ registrados juntos.
|
|
|
136
121
|
| | V1 | V2 |
|
|
137
122
|
|---|---|---|
|
|
138
123
|
| Chave de config do OpenCode | `plugin` singular | `plugins` plural |
|
|
139
|
-
| Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` |
|
|
140
|
-
| Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2: 3 ferramentas (`hashline_edit`, `pantheon_cost`, `pantheon_model`),
|
|
124
|
+
| Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | `pantheon-opencode/plugin-v2` (`src/plugin-v2.ts`) |
|
|
125
|
+
| Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 3 ferramentas (`hashline_edit`, `pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos, session hooks (`prompt`, `context`), um hook de ferramenta `execute.before` que impõe sessões somente-leitura, além de transforms de configuração |
|
|
141
126
|
| APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
|
|
142
127
|
|
|
143
128
|
O plugin V2 fornece 3 ferramentas de orquestração (`hashline_edit`,
|
|
144
|
-
`pantheon_cost`, `pantheon_model`),
|
|
145
|
-
`session.idle`, `session.
|
|
129
|
+
`pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
|
|
130
|
+
`session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
|
|
146
131
|
`context`) e um hook de ferramenta `execute.before` que impõe sessões
|
|
147
132
|
somente-leitura. As 3 ferramentas de goal **não** são
|
|
148
133
|
registradas no V2: o goal loop precisa de um `GoalStore`, de um
|
|
@@ -174,18 +159,15 @@ toda chamada. Os recursos V2 sem suporte são `legacy-hooks` (a superfície de
|
|
|
174
159
|
hooks específica do V1), `catalog-transform`, `integration-transform`,
|
|
175
160
|
`skill-transform` e `goal-tools`.
|
|
176
161
|
|
|
177
|
-
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
editadas manualmente devem usar o array nativo `permissions` com
|
|
182
|
-
`action`/`resource`/`effect`.
|
|
162
|
+
O pacote expõe os dois contratos como exports importáveis:
|
|
163
|
+
`pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
|
|
164
|
+
`pantheon-opencode/v2-bridge` (interop opcional), para que o host carregue
|
|
165
|
+
explicitamente o contrato desejado.
|
|
183
166
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
as ferramentas de goal/board do V1.
|
|
167
|
+
A ponte V1→V2 (`src/pantheon/v2-bridge.ts`) habilita interop opcional:
|
|
168
|
+
singletons de infraestrutura V1 (BackgroundJobBoard,
|
|
169
|
+
GoalStore, TodoEnforcer, VisionHandler) são repassados via `ctx.options` do V2.
|
|
170
|
+
A ponte é opcional — o V2 funciona standalone com degradação graciosa.
|
|
189
171
|
|
|
190
172
|
Selecione o contrato explicitamente na instalação:
|
|
191
173
|
|
|
@@ -262,8 +244,8 @@ pontos. A ordem das chaves não muda o resultado.
|
|
|
262
244
|
Um comando mantém uma instalação existente em dia:
|
|
263
245
|
|
|
264
246
|
```bash
|
|
265
|
-
# Sem instalação global, fixe o dist-tag
|
|
266
|
-
#
|
|
247
|
+
# Sem instalação global, SEMPRE fixe o dist-tag — `npx pantheon-opencode`
|
|
248
|
+
# puro resolve a tag `latest`, que é o release estável (1.4.3):
|
|
267
249
|
npx pantheon-opencode@beta update # canal beta do npm + refresh do config
|
|
268
250
|
npx pantheon-opencode@beta update --stable # canal estável
|
|
269
251
|
|
|
@@ -15,7 +15,13 @@ agent: "zeus"
|
|
|
15
15
|
## Layers
|
|
16
16
|
|
|
17
17
|
### Layer 1 — Heuristic Scan
|
|
18
|
-
|
|
18
|
+
Resolve the installed helper first (do not assume the current project contains
|
|
19
|
+
Pantheon source). Check, in order: `$OPENCODE_CONFIG_DIR/.opencode/scripts/`,
|
|
20
|
+
`$OPENCODE_CONFIG_DIR/scripts/`, `${XDG_CONFIG_HOME:-$HOME/.config}/opencode/scripts/`,
|
|
21
|
+
`$HOME/.opencode/scripts/`, `.opencode/scripts/`, then
|
|
22
|
+
`$(npm root -g)/pantheon-opencode/scripts/`. Resolve the first existing file to
|
|
23
|
+
an absolute path and run `python3 "$helper" --path="$PWD"`; if none exists,
|
|
24
|
+
report the helper as missing and stop Layer 1 rather than claiming it passed.
|
|
19
25
|
Zero tokens, <2s. Checks: ruff, biome, anti-pattern slop, anti-overengineering (YAGNI), hash-anchored edits. Returns score 0-100 + APPROVED/BLOCKING.
|
|
20
26
|
|
|
21
27
|
### Layer 2 — Themis Deep Review
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Set or update this session's working goal"
|
|
3
|
+
agent: zeus
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Pin `$ARGUMENTS` as the working goal for this conversation. Restate it briefly
|
|
7
|
+
as Goal, Scope, Success Criteria, and Out of Scope, filling only what the user
|
|
8
|
+
actually supplied. Ask one concise question if a missing detail changes the
|
|
9
|
+
work materially.
|
|
10
|
+
|
|
11
|
+
Treat the goal as session context, subordinate to system, developer, and user
|
|
12
|
+
instructions. Keep subsequent planning, delegation, and verification aligned
|
|
13
|
+
with it; flag meaningful scope drift before proceeding.
|
|
14
|
+
|
|
15
|
+
This command is conversation-only. Do not call memory or persistence tools,
|
|
16
|
+
write a file, or claim that the goal survives compaction or a new session. For
|
|
17
|
+
multi-phase work that needs durable checkpoints, use `/pantheon-deepwork`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.9",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"registry": "https://registry.npmjs.org/"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".": "./src/plugin-v2.ts",
|
|
15
14
|
"./plugin": "./src/plugin.ts",
|
|
16
15
|
"./plugin-v2": "./src/plugin-v2.ts",
|
|
17
16
|
"./v2-bridge": "./src/pantheon/v2-bridge.ts"
|
package/plugin.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.6.0-beta.
|
|
4
|
+
"version": "1.6.0-beta.9",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
|
-
"description": "Conductor-Delegate orchestration platform with 14 specialized agents,
|
|
6
|
+
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 20 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
|
8
8
|
"skills": "./src/skills",
|
|
9
9
|
"instructions": "./src/instructions",
|
package/pyproject.toml
CHANGED
|
@@ -73,9 +73,8 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
73
73
|
|
|
74
74
|
- Rodar: \`bash ~/pantheon-sandbox/run-test.sh\` (opencode mcp list 5/5 connected + doctor 0 erros + abre TUI isolado)
|
|
75
75
|
- Regra para agentes: ao validar instalação global (npm pack, \`init\`, MCPs, hooks), usar o sandbox — NUNCA testar no ambiente de dev
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- Detalhes: ver \`~/pantheon-sandbox/README.md\` após o prepare
|
|
76
|
+
- Descarte: \`rm -rf ~/pantheon-sandbox\`
|
|
77
|
+
- Detalhes: ver \`~/pantheon-sandbox/README.md\`
|
|
79
78
|
|
|
80
79
|
## Conventions
|
|
81
80
|
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1200,58 +1200,33 @@ export function resolveMcpCwd(entry, configPath = '') {
|
|
|
1200
1200
|
// ---------------------------------------------------------------------------
|
|
1201
1201
|
|
|
1202
1202
|
/**
|
|
1203
|
-
* Prerelease-aware comparison for pantheon versions (X.Y.Z
|
|
1204
|
-
*
|
|
1205
|
-
* outranks any prerelease of the same core version; two prereleases compare
|
|
1206
|
-
* identifier by identifier, numeric identifiers numerically. Returns
|
|
1203
|
+
* Prerelease-aware comparison for pantheon versions (X.Y.Z or X.Y.Z-beta.N).
|
|
1204
|
+
* A stable release outranks any beta of the same core version. Returns
|
|
1207
1205
|
* <0, 0, >0 (a - b). The shared compareVersions/parseVersion above ignores
|
|
1208
|
-
* the
|
|
1209
|
-
* every
|
|
1206
|
+
* the `-beta.N` suffix (it only reads 4 numeric groups), which would make
|
|
1207
|
+
* every beta pair compare equal.
|
|
1210
1208
|
*/
|
|
1211
1209
|
function comparePantheonVersions(a, b) {
|
|
1212
1210
|
const parse = (v) => {
|
|
1213
1211
|
const m = String(v)
|
|
1214
1212
|
.trim()
|
|
1215
|
-
.match(/^(\d+)\.(\d+)\.(\d+)(?:-(
|
|
1213
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
|
|
1216
1214
|
if (!m) return null
|
|
1217
1215
|
return {
|
|
1218
1216
|
core: [+m[1], +m[2], +m[3]],
|
|
1219
|
-
|
|
1217
|
+
beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
|
|
1220
1218
|
}
|
|
1221
1219
|
}
|
|
1222
|
-
// Per semver precedence: numeric identifiers compare numerically and rank
|
|
1223
|
-
// below alphanumeric ones; a shorter identifier list ranks below a longer
|
|
1224
|
-
// one sharing its prefix; a version WITHOUT a prerelease outranks any
|
|
1225
|
-
// prerelease of the same core version.
|
|
1226
|
-
const comparePrerelease = (pa, pb) => {
|
|
1227
|
-
if (pa === null && pb === null) return 0
|
|
1228
|
-
if (pa === null) return 1
|
|
1229
|
-
if (pb === null) return -1
|
|
1230
|
-
const length = Math.max(pa.length, pb.length)
|
|
1231
|
-
for (let i = 0; i < length; i++) {
|
|
1232
|
-
const x = pa[i]
|
|
1233
|
-
const y = pb[i]
|
|
1234
|
-
if (x === undefined) return -1
|
|
1235
|
-
if (y === undefined) return 1
|
|
1236
|
-
const xNumeric = /^\d+$/.test(x)
|
|
1237
|
-
const yNumeric = /^\d+$/.test(y)
|
|
1238
|
-
if (xNumeric && yNumeric) {
|
|
1239
|
-
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
1240
|
-
} else if (xNumeric !== yNumeric) {
|
|
1241
|
-
return xNumeric ? -1 : 1
|
|
1242
|
-
} else if (x !== y) {
|
|
1243
|
-
return x < y ? -1 : 1
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return 0
|
|
1247
|
-
}
|
|
1248
1220
|
const pa = parse(a)
|
|
1249
1221
|
const pb = parse(b)
|
|
1250
1222
|
if (!pa || !pb) return 0
|
|
1251
1223
|
for (let i = 0; i < 3; i++) {
|
|
1252
1224
|
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1253
1225
|
}
|
|
1254
|
-
|
|
1226
|
+
// Infinity - Infinity is NaN, which would make equal stable versions compare
|
|
1227
|
+
// as drift for any caller using a strict === 0 check.
|
|
1228
|
+
if (pa.beta === pb.beta) return 0
|
|
1229
|
+
return pa.beta < pb.beta ? -1 : 1
|
|
1255
1230
|
}
|
|
1256
1231
|
|
|
1257
1232
|
/**
|
|
@@ -1317,24 +1292,8 @@ export function checkInstallVersionDrift(args) {
|
|
|
1317
1292
|
// registered plugin points into against this package's own version. Without
|
|
1318
1293
|
// network access the "latest" version IS the running package's version.
|
|
1319
1294
|
|
|
1320
|
-
// Single source of truth: the package name is read from package.json, the same
|
|
1321
|
-
// way scripts/install/opencode.mjs derives the V2 pin spec. The fallback keeps
|
|
1322
|
-
// the doctor usable on a broken manifest.
|
|
1323
|
-
const PACKAGE_MANIFEST = readJson(join(ROOT, 'package.json'))
|
|
1324
|
-
const PANTHEON_PACKAGE_NAME =
|
|
1325
|
-
typeof PACKAGE_MANIFEST?.name === 'string' && PACKAGE_MANIFEST.name
|
|
1326
|
-
? PACKAGE_MANIFEST.name
|
|
1327
|
-
: 'pantheon-opencode'
|
|
1328
|
-
// Matches the exact version-pinned V2 spec the installer writes
|
|
1329
|
-
// (`<name>@X.Y.Z` plus any semver prerelease suffix). The name is escaped
|
|
1330
|
-
// because package names may contain regex-significant characters (`.` in
|
|
1331
|
-
// unscoped names, `/` in scoped ones).
|
|
1332
|
-
const PINNED_PLUGIN_SPEC_PATTERN = new RegExp(
|
|
1333
|
-
`^${PANTHEON_PACKAGE_NAME.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$`,
|
|
1334
|
-
)
|
|
1335
|
-
|
|
1336
1295
|
/** Locates an installed copy of this package inside a dependency tree. */
|
|
1337
|
-
const INSTALLED_PACKAGE_MARKER =
|
|
1296
|
+
const INSTALLED_PACKAGE_MARKER = '/node_modules/pantheon-opencode/'
|
|
1338
1297
|
|
|
1339
1298
|
/**
|
|
1340
1299
|
* Return the installed-package root containing a plugin path, or null when the
|
|
@@ -1403,10 +1362,8 @@ export function collectRegisteredPluginRefs(configs) {
|
|
|
1403
1362
|
if (typeof ref === 'string') {
|
|
1404
1363
|
refs.push({ path: ref, configPath: cfg.path, configLabel: cfg.label })
|
|
1405
1364
|
} else if (ref && typeof ref === 'object') {
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1408
|
-
// exact pinned npm version as well as installed filesystem copies.
|
|
1409
|
-
const objectPath = ['package', 'path', 'source', 'id', 'name'].find(
|
|
1365
|
+
// Object-shaped entries carry their path under `path`/`source`.
|
|
1366
|
+
const objectPath = ['path', 'source', 'id', 'name'].find(
|
|
1410
1367
|
(k) => typeof ref[k] === 'string',
|
|
1411
1368
|
)
|
|
1412
1369
|
if (objectPath) {
|
|
@@ -1419,20 +1376,6 @@ export function collectRegisteredPluginRefs(configs) {
|
|
|
1419
1376
|
return refs
|
|
1420
1377
|
}
|
|
1421
1378
|
|
|
1422
|
-
/**
|
|
1423
|
-
* Read an exact version from the package-root plugin syntax written by the
|
|
1424
|
-
* V2 installer (`pantheon-opencode@1.6.0`), including ANY semver prerelease
|
|
1425
|
-
* suffix (`-beta.11`, `-rc.1`, ...). Unpinned and unrelated packages
|
|
1426
|
-
* deliberately return null: H3 cannot infer their resolved npm cache version.
|
|
1427
|
-
* @param {string} pluginRef
|
|
1428
|
-
* @returns {string|null}
|
|
1429
|
-
*/
|
|
1430
|
-
function readPinnedPantheonPluginVersion(pluginRef) {
|
|
1431
|
-
if (typeof pluginRef !== 'string') return null
|
|
1432
|
-
const match = pluginRef.match(PINNED_PLUGIN_SPEC_PATTERN)
|
|
1433
|
-
return match?.[1] ?? null
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
1379
|
/**
|
|
1437
1380
|
* Compare the version of the package each registered plugin points into (when
|
|
1438
1381
|
* it is an installed copy under node_modules/pantheon-opencode) against this
|
|
@@ -1461,21 +1404,6 @@ export function checkPluginVersionDrift(args) {
|
|
|
1461
1404
|
let compared = 0
|
|
1462
1405
|
let driftReported = false
|
|
1463
1406
|
for (const ref of refs) {
|
|
1464
|
-
const pinnedNpmVersion = readPinnedPantheonPluginVersion(ref.path)
|
|
1465
|
-
if (pinnedNpmVersion) {
|
|
1466
|
-
const identity = `npm:${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}`
|
|
1467
|
-
if (seen.has(identity)) continue
|
|
1468
|
-
seen.add(identity)
|
|
1469
|
-
compared++
|
|
1470
|
-
|
|
1471
|
-
if (classifyPluginVersionDrift(pinnedNpmVersion, packageVersion) !== 'drift') continue
|
|
1472
|
-
driftReported = true
|
|
1473
|
-
warn(
|
|
1474
|
-
`${ref.configLabel}: V2 plugin is pinned to ${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}, but this package is v${packageVersion} — the registered tool surface may be stale. Run \`npx pantheon-opencode init\` to realign the pinned plugin version`,
|
|
1475
|
-
)
|
|
1476
|
-
continue
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
1407
|
const absolute = isAbsolute(ref.path) ? ref.path : resolve(dirname(ref.configPath), ref.path)
|
|
1480
1408
|
const installed = readInstalledPluginVersion(absolute)
|
|
1481
1409
|
if (!installed) continue
|
|
@@ -1514,29 +1442,21 @@ export function checkPluginVersionDrift(args) {
|
|
|
1514
1442
|
* The V2 contract is the PLURAL `plugins` key; V1 is the singular `plugin` key.
|
|
1515
1443
|
* So the key alone is not the test — a config can legitimately carry a
|
|
1516
1444
|
* third-party entry in `plugins`. What identifies the generation is a Pantheon
|
|
1517
|
-
* V2 entry inside it.
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
* similarly named third-party plugins.
|
|
1445
|
+
* V2 entry inside it. Matched on the `plugin-v2` marker so all three real
|
|
1446
|
+
* spellings are recognised: the canonical DIRECTORY (`src/plugin-v2`), an
|
|
1447
|
+
* absolute path to it, and the package export (`pantheon-opencode/plugin-v2`).
|
|
1521
1448
|
*
|
|
1522
1449
|
* @param {unknown} ref
|
|
1523
1450
|
* @returns {boolean}
|
|
1524
1451
|
*/
|
|
1525
1452
|
function isV2PluginRef(ref) {
|
|
1526
|
-
const
|
|
1453
|
+
const candidate =
|
|
1527
1454
|
typeof ref === 'string'
|
|
1528
|
-
?
|
|
1455
|
+
? ref
|
|
1529
1456
|
: ref && typeof ref === 'object'
|
|
1530
|
-
?
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
: []
|
|
1534
|
-
return candidates.some(
|
|
1535
|
-
(candidate) =>
|
|
1536
|
-
candidate.includes('plugin-v2') ||
|
|
1537
|
-
candidate === PANTHEON_PACKAGE_NAME ||
|
|
1538
|
-
candidate.startsWith(`${PANTHEON_PACKAGE_NAME}@`),
|
|
1539
|
-
)
|
|
1457
|
+
? (ref.path ?? ref.source ?? ref.id ?? ref.name)
|
|
1458
|
+
: undefined
|
|
1459
|
+
return typeof candidate === 'string' && candidate.includes('plugin-v2')
|
|
1540
1460
|
}
|
|
1541
1461
|
|
|
1542
1462
|
/**
|
|
@@ -1912,9 +1832,7 @@ export function classifyNodeSqliteProbe(probe) {
|
|
|
1912
1832
|
if (probe.available) {
|
|
1913
1833
|
return {
|
|
1914
1834
|
status: 'ok',
|
|
1915
|
-
message:
|
|
1916
|
-
`node:sqlite available on ${probe.version} — read-only SQLite backend prerequisite satisfied; ` +
|
|
1917
|
-
'doctor does not verify pantheon_cost tool registration',
|
|
1835
|
+
message: `node:sqlite available on ${probe.version} — pantheon_cost read-only backend supported`,
|
|
1918
1836
|
}
|
|
1919
1837
|
}
|
|
1920
1838
|
return {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scan-secrets.sh — Pantheon PreToolUse Secret Scanning Hook
|
|
3
|
+
# Detects hardcoded secrets in tool input. Logs ALWAYS (masked); the exit
|
|
4
|
+
# code drives the plugin's hybrid blocking (see pantheon-hooks.ts):
|
|
5
|
+
#
|
|
6
|
+
# exit 0 — no match.
|
|
7
|
+
# exit 1 — LOW_CONFIDENCE match only (header/KEY NAMES such as the Bifrost header
|
|
8
|
+
# alone, api_key=/password=/secret= without a recognizable token
|
|
9
|
+
# value). Advisory: the plugin logs + toasts, does NOT block.
|
|
10
|
+
# exit 2 — HIGH_CONFIDENCE match (real token formats: sk-bf-<token>,
|
|
11
|
+
# AKIA..., ghp_..., glpat-..., sk_live_/sk_test_..., xox[baprs]-...,
|
|
12
|
+
# Bearer <token>, JWT). The plugin BLOCKS the tool call (throws
|
|
13
|
+
# after logging — the only deliberate throw in the plugin).
|
|
14
|
+
#
|
|
15
|
+
# Matches are always MASKED (first 4 + last 4 chars for len>8, else "****")
|
|
16
|
+
# before being echoed — the hook trail must never contain a raw secret value.
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
HIGH_CONFIDENCE_PATTERNS=(
|
|
20
|
+
"AKIA[0-9A-Z]{16}"
|
|
21
|
+
"gh[pousr]_[A-Za-z0-9_]{36,}"
|
|
22
|
+
"glpat-[A-Za-z0-9_\\-]{20}"
|
|
23
|
+
"sk-[a-zA-Z0-9]{20,}"
|
|
24
|
+
"sk_live_[a-zA-Z0-9]{20,}"
|
|
25
|
+
"sk_test_[a-zA-Z0-9]{20,}"
|
|
26
|
+
"xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}"
|
|
27
|
+
"bearer\\s+[a-zA-Z0-9_\\-\\.]{20,}"
|
|
28
|
+
"eyJ[A-Za-z0-9_-]*\\.eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
LOW_CONFIDENCE_PATTERNS=(
|
|
32
|
+
# KEY/header NAMES without a recognizable token value — high false-positive
|
|
33
|
+
# rate (e.g. the Bifrost header alone, api_key=longvalue, password="x", secret="y"):
|
|
34
|
+
# advisory only, never blocks.
|
|
35
|
+
"[a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"']?[a-zA-Z0-9_\\-]{16,}[\"']?"
|
|
36
|
+
"[a-zA-Z0-9_-]*password[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"'][^\"']{8,}[\"']"
|
|
37
|
+
"[a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"'][^\"']{8,}[\"']"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Keep provider-specific markers assembled so this scanner is not self-matched
|
|
41
|
+
# by the repository secret test.
|
|
42
|
+
BIFROST_HEADER="x""-bf-""vk"
|
|
43
|
+
BIFROST_TOKEN_PREFIX="sk""-bf-"
|
|
44
|
+
HIGH_CONFIDENCE_PATTERNS+=("${BIFROST_TOKEN_PREFIX}[A-Za-z0-9_-]{8,}")
|
|
45
|
+
LOW_CONFIDENCE_PATTERNS+=("$BIFROST_HEADER")
|
|
46
|
+
|
|
47
|
+
# Mask a matched secret for safe logging: first 4 + last 4 chars (len > 8),
|
|
48
|
+
# else fully hidden — short matches must not be reconstructable by
|
|
49
|
+
# concatenating the visible slices.
|
|
50
|
+
mask_secret() {
|
|
51
|
+
local match="$1"
|
|
52
|
+
if [[ ${#match} -le 8 ]]; then
|
|
53
|
+
echo "****"
|
|
54
|
+
else
|
|
55
|
+
echo "${match:0:4}****${match: -4}"
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
INPUT="${1:-${TOOL_INPUT:-}}"
|
|
60
|
+
if [[ -z "$INPUT" ]] && [[ ! -t 0 ]]; then
|
|
61
|
+
INPUT=$(cat)
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [[ -z "$INPUT" ]]; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
FOUND_HIGH=0
|
|
69
|
+
FOUND_LOW=0
|
|
70
|
+
|
|
71
|
+
for pattern in "${HIGH_CONFIDENCE_PATTERNS[@]}"; do
|
|
72
|
+
if echo "$INPUT" | grep -iqE "$pattern"; then
|
|
73
|
+
MATCH=$(echo "$INPUT" | grep -ioE "$pattern" | head -1)
|
|
74
|
+
echo "[SECRET SCAN] High-confidence secret detected: $(mask_secret "$MATCH") (pattern: $pattern)" >&2
|
|
75
|
+
FOUND_HIGH=1
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
for pattern in "${LOW_CONFIDENCE_PATTERNS[@]}"; do
|
|
80
|
+
if echo "$INPUT" | grep -iqE "$pattern"; then
|
|
81
|
+
MATCH=$(echo "$INPUT" | grep -ioE "$pattern" | head -1)
|
|
82
|
+
echo "[SECRET SCAN] Potential secret detected (low confidence): $(mask_secret "$MATCH") (pattern: $pattern)" >&2
|
|
83
|
+
FOUND_LOW=1
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
if [[ $FOUND_HIGH -eq 1 ]]; then
|
|
88
|
+
echo "[SECRET SCAN] High-confidence hardcoded secret detected. Tool call will be BLOCKED." >&2
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [[ $FOUND_LOW -eq 1 ]]; then
|
|
93
|
+
echo "[SECRET SCAN] Possible secret detected. Remove it and use environment variables or a vault." >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
exit 0
|
|
@@ -29,25 +29,14 @@ function deepClone(obj) {
|
|
|
29
29
|
// V1 → V2 helpers
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
-
/** Action name mapping for permissions (V1 key → V2 action) */
|
|
32
|
+
/** Action name mapping for permissions (V1 key → V2 action), including task → subagent. */
|
|
33
33
|
const V1_PERMISSION_TO_ACTION = {
|
|
34
34
|
bash: 'shell',
|
|
35
|
-
skill: 'skill',
|
|
36
|
-
edit: 'edit',
|
|
37
|
-
// V2 folds V1's `write` tool permission into the `edit` action, which also
|
|
38
|
-
// governs write and patch tools. Keep `edit` as the canonical reverse name.
|
|
39
|
-
write: 'edit',
|
|
40
35
|
task: 'subagent',
|
|
41
|
-
websearch: 'websearch',
|
|
42
|
-
// passthrough unknowns
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const V2_ACTION_TO_V1_PERMISSION = {
|
|
46
|
-
shell: 'bash',
|
|
47
36
|
skill: 'skill',
|
|
48
37
|
edit: 'edit',
|
|
49
|
-
subagent: 'task',
|
|
50
38
|
websearch: 'websearch',
|
|
39
|
+
// passthrough unknowns
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
/**
|
|
@@ -61,41 +50,15 @@ const V2_ACTION_TO_V1_PERMISSION = {
|
|
|
61
50
|
*/
|
|
62
51
|
function convertPermissionsV1toV2(permObj) {
|
|
63
52
|
const result = []
|
|
64
|
-
const resultIndices = new Map()
|
|
65
|
-
const effectRank = { allow: 0, ask: 1, deny: 2 }
|
|
66
|
-
|
|
67
|
-
const appendPermission = (action, resource, effect) => {
|
|
68
|
-
const key = JSON.stringify([action, resource])
|
|
69
|
-
const previousIndex = resultIndices.get(key)
|
|
70
|
-
if (previousIndex === undefined) {
|
|
71
|
-
resultIndices.set(key, result.length)
|
|
72
|
-
result.push({ action, resource, effect })
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const previous = result[previousIndex]
|
|
77
|
-
if (previous.effect === effect) return
|
|
78
|
-
|
|
79
|
-
// V1 keeps `edit` and `write` as separate permission keys; V2's `edit`
|
|
80
|
-
// action covers both. If their rules collide on one resource, retain the
|
|
81
|
-
// stricter effect rather than letting object order turn a deny into allow.
|
|
82
|
-
console.warn(
|
|
83
|
-
`[config-migration] Conflicting V1 permission aliases for ${action}:${resource}; keeping the more restrictive effect`,
|
|
84
|
-
)
|
|
85
|
-
if ((effectRank[effect] ?? 0) > (effectRank[previous.effect] ?? 0)) {
|
|
86
|
-
previous.effect = effect
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
53
|
for (const [key, value] of Object.entries(permObj)) {
|
|
91
54
|
const action = V1_PERMISSION_TO_ACTION[key] || key
|
|
92
55
|
if (typeof value === 'string') {
|
|
93
56
|
// Simple: { edit: "allow" } → [{ action: "edit", resource: "*", effect: "allow" }]
|
|
94
|
-
|
|
57
|
+
result.push({ action, resource: '*', effect: value })
|
|
95
58
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
96
59
|
// Nested: { bash: { "git *": "allow" } } → [{ action: "shell", resource: "git *", effect: "allow" }]
|
|
97
60
|
for (const [resource, effect] of Object.entries(value)) {
|
|
98
|
-
|
|
61
|
+
result.push({ action, resource, effect })
|
|
99
62
|
}
|
|
100
63
|
} else {
|
|
101
64
|
console.warn(`[config-migration] Unexpected permission value for "${key}":`, value)
|
|
@@ -109,9 +72,13 @@ function convertPermissionsV1toV2(permObj) {
|
|
|
109
72
|
*/
|
|
110
73
|
function convertPermissionsV2toV1(permArray) {
|
|
111
74
|
const result = {}
|
|
75
|
+
// Reverse action mapping: V2 action → V1 key
|
|
76
|
+
const ACTION_TO_V1 = Object.fromEntries(
|
|
77
|
+
Object.entries(V1_PERMISSION_TO_ACTION).map(([k, v]) => [v, k]),
|
|
78
|
+
)
|
|
112
79
|
|
|
113
80
|
for (const { action, resource, effect } of permArray) {
|
|
114
|
-
const v1Key =
|
|
81
|
+
const v1Key = ACTION_TO_V1[action] || action
|
|
115
82
|
if (resource === '*') {
|
|
116
83
|
result[v1Key] = effect
|
|
117
84
|
} else {
|
|
@@ -329,7 +329,7 @@ export async function runInteractiveInstall({ components = [], defaultComponents
|
|
|
329
329
|
{
|
|
330
330
|
name: 'skills',
|
|
331
331
|
value: 'skills',
|
|
332
|
-
description: `Reusable skill workflows (${
|
|
332
|
+
description: `Reusable skill workflows (${20} skills)`,
|
|
333
333
|
},
|
|
334
334
|
{
|
|
335
335
|
name: 'instructions',
|