pantheon-opencode 1.6.0-beta.7 → 1.6.0-beta.8
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 +37 -100
- package/README.md +78 -102
- package/README.pt-BR.md +19 -37
- 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 +114 -43
- package/scripts/install/strings.mjs +0 -8
- package/scripts/probe-pantheon-cost.mjs +3 -16
- package/scripts/test-opencode-v2-sandbox.sh +203 -786
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +3 -4
- 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 +14 -42
- 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.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +26 -50
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +25 -60
- 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/pantheon/secret-scanner.ts +0 -489
- package/src/plugins/tui/README.md +0 -14
- package/src/skills/council-synthesis/SKILL.md +0 -29
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import { types as utilTypes } from 'node:util'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-process runtime secret scanner shared by the OpenCode V1 and V2 hooks.
|
|
5
|
-
*
|
|
6
|
-
* Patterns preserve the former shell scanner semantics: high-confidence
|
|
7
|
-
* formats deny, while key/header names and generic assignments are advisory.
|
|
8
|
-
* Findings contain only the former scanner's masked representation;
|
|
9
|
-
* neither input nor raw matches are returned or logged here.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export type SecretScanFinding = {
|
|
13
|
-
confidence: 'high' | 'low'
|
|
14
|
-
pattern: string
|
|
15
|
-
masked: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type SecretScanResult =
|
|
19
|
-
| { status: 'clean' }
|
|
20
|
-
| { status: 'invalid' }
|
|
21
|
-
| { status: 'block'; findings: SecretScanFinding[] }
|
|
22
|
-
| { status: 'advisory'; findings: SecretScanFinding[] }
|
|
23
|
-
|
|
24
|
-
/** Maximum UTF-8 size of the serialized scanner input (5 MiB). */
|
|
25
|
-
export const MAX_SECRET_SCAN_INPUT_BYTES = 5 * 1024 * 1024
|
|
26
|
-
|
|
27
|
-
type SecretPattern = {
|
|
28
|
-
name: string
|
|
29
|
-
confidence: 'high' | 'low'
|
|
30
|
-
regex: RegExp
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const bifrostTokenPrefix = ['sk', '-bf-'].join('')
|
|
34
|
-
const bifrostHeader = ['x', '-bf-', 'vk'].join('')
|
|
35
|
-
const notLineFeedWhitespace = String.raw`[^\S\n]`
|
|
36
|
-
const IDENTIFIER_TOKEN_RE = /[A-Za-z0-9_-]+/g
|
|
37
|
-
const WHITESPACE_RE = /\s/
|
|
38
|
-
|
|
39
|
-
// Compile all runtime detection expressions once when this shared module loads.
|
|
40
|
-
// Do not add the repository-hygiene patterns from scripts/secret-scan.mjs here.
|
|
41
|
-
// The expressions use bounded character classes and fixed separators; none has
|
|
42
|
-
// nested ambiguous quantifiers. The 5 MiB input cap also bounds their linear
|
|
43
|
-
// scans and the assignment/JWT parsing loops below.
|
|
44
|
-
const SECRET_PATTERNS: SecretPattern[] = [
|
|
45
|
-
{ name: 'aws-access-key', confidence: 'high', regex: /AKIA[0-9A-Z]{16}/i },
|
|
46
|
-
{ name: 'github-token', confidence: 'high', regex: /gh[pousr]_[A-Za-z0-9_]{36,}/i },
|
|
47
|
-
{ name: 'gitlab-token', confidence: 'high', regex: /glpat-[A-Za-z0-9_-]{20}/i },
|
|
48
|
-
{ name: 'api-token', confidence: 'high', regex: /sk-[A-Za-z0-9]{20,}/i },
|
|
49
|
-
{ name: 'live-api-token', confidence: 'high', regex: /sk_live_[A-Za-z0-9]{20,}/i },
|
|
50
|
-
{ name: 'test-api-token', confidence: 'high', regex: /sk_test_[A-Za-z0-9]{20,}/i },
|
|
51
|
-
{
|
|
52
|
-
name: 'messaging-token',
|
|
53
|
-
confidence: 'high',
|
|
54
|
-
regex: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{24}/i,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'bearer-token',
|
|
58
|
-
confidence: 'high',
|
|
59
|
-
regex: new RegExp(`bearer${notLineFeedWhitespace}+[A-Za-z0-9_.-]{20,}`, 'i'),
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: 'jwt',
|
|
63
|
-
confidence: 'high',
|
|
64
|
-
regex: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/i,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'bifrost-token',
|
|
68
|
-
confidence: 'high',
|
|
69
|
-
regex: new RegExp(`${bifrostTokenPrefix}[A-Za-z0-9_-]{8,}`, 'i'),
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'api-key-assignment',
|
|
73
|
-
confidence: 'low',
|
|
74
|
-
regex: /api[_-]?key/i,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'password-assignment',
|
|
78
|
-
confidence: 'low',
|
|
79
|
-
regex: /password/i,
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'secret-assignment',
|
|
83
|
-
confidence: 'low',
|
|
84
|
-
regex: /secret/i,
|
|
85
|
-
},
|
|
86
|
-
{ name: 'bifrost-header', confidence: 'low', regex: new RegExp(bifrostHeader, 'i') },
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
export const SECRET_SCAN_PATTERN_COUNT = SECRET_PATTERNS.length
|
|
90
|
-
|
|
91
|
-
const MAX_SECRET_SCAN_JSON_DEPTH = 512
|
|
92
|
-
|
|
93
|
-
type JsonPreflightFrame =
|
|
94
|
-
| { kind: 'value'; value: unknown; depth: number }
|
|
95
|
-
| {
|
|
96
|
-
kind: 'array'
|
|
97
|
-
value: unknown[]
|
|
98
|
-
index: number
|
|
99
|
-
length: number
|
|
100
|
-
depth: number
|
|
101
|
-
}
|
|
102
|
-
| {
|
|
103
|
-
kind: 'object'
|
|
104
|
-
value: Record<string, unknown>
|
|
105
|
-
keys: IterableIterator<string>
|
|
106
|
-
first: boolean
|
|
107
|
-
depth: number
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function serializedStringBytes(value: string, remainingBytes: number): number | undefined {
|
|
111
|
-
let bytes = 2 // The JSON quotes.
|
|
112
|
-
if (bytes > remainingBytes) return undefined
|
|
113
|
-
|
|
114
|
-
for (let index = 0; index < value.length; index++) {
|
|
115
|
-
const code = value.charCodeAt(index)
|
|
116
|
-
if (
|
|
117
|
-
code === 0x22 ||
|
|
118
|
-
code === 0x5c ||
|
|
119
|
-
code === 0x08 ||
|
|
120
|
-
code === 0x09 ||
|
|
121
|
-
code === 0x0a ||
|
|
122
|
-
code === 0x0c ||
|
|
123
|
-
code === 0x0d
|
|
124
|
-
) {
|
|
125
|
-
bytes += 2
|
|
126
|
-
} else if (code < 0x20) {
|
|
127
|
-
bytes += 6
|
|
128
|
-
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
129
|
-
const next = value.charCodeAt(index + 1)
|
|
130
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
131
|
-
bytes += 4
|
|
132
|
-
index++
|
|
133
|
-
} else {
|
|
134
|
-
// Well-formed JSON.stringify escapes unpaired surrogates as \udxxx.
|
|
135
|
-
bytes += 6
|
|
136
|
-
}
|
|
137
|
-
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
138
|
-
bytes += 6
|
|
139
|
-
} else if (code <= 0x7f) {
|
|
140
|
-
bytes++
|
|
141
|
-
} else if (code <= 0x7ff) {
|
|
142
|
-
bytes += 2
|
|
143
|
-
} else {
|
|
144
|
-
bytes += 3
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (bytes > remainingBytes) return undefined
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return bytes
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function ownEnumerableKeys(value: Record<string, unknown>): IterableIterator<string> {
|
|
154
|
-
return (function* (): IterableIterator<string> {
|
|
155
|
-
for (const key in value) {
|
|
156
|
-
if (Object.hasOwn(value, key)) yield key
|
|
157
|
-
}
|
|
158
|
-
})()
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Measure JSON.stringify's compact UTF-8 output without creating serialized
|
|
163
|
-
* strings. Returns undefined for oversized or non-plain JSON data.
|
|
164
|
-
*/
|
|
165
|
-
function measureJsonBytes(value: unknown, limit: number): number | undefined {
|
|
166
|
-
let bytes = 0
|
|
167
|
-
const active = new WeakSet<object>()
|
|
168
|
-
const stack: JsonPreflightFrame[] = [{ kind: 'value', value, depth: 0 }]
|
|
169
|
-
|
|
170
|
-
while (stack.length > 0) {
|
|
171
|
-
const frame = stack[stack.length - 1]
|
|
172
|
-
if (frame === undefined) return undefined
|
|
173
|
-
|
|
174
|
-
if (frame.kind === 'value') {
|
|
175
|
-
stack.pop()
|
|
176
|
-
const current = frame.value
|
|
177
|
-
if (current === null) {
|
|
178
|
-
bytes += 4
|
|
179
|
-
} else if (typeof current === 'string') {
|
|
180
|
-
const stringBytes = serializedStringBytes(current, limit - bytes)
|
|
181
|
-
if (stringBytes === undefined) return undefined
|
|
182
|
-
bytes += stringBytes
|
|
183
|
-
} else if (typeof current === 'boolean') {
|
|
184
|
-
bytes += current ? 4 : 5
|
|
185
|
-
} else if (typeof current === 'number') {
|
|
186
|
-
if (!Number.isFinite(current)) return undefined
|
|
187
|
-
// Number#toString uses JSON's finite-number spelling, including -0 => 0.
|
|
188
|
-
bytes += String(current).length
|
|
189
|
-
} else if (typeof current !== 'object' || utilTypes.isProxy(current)) {
|
|
190
|
-
return undefined
|
|
191
|
-
} else {
|
|
192
|
-
if (frame.depth >= MAX_SECRET_SCAN_JSON_DEPTH || active.has(current)) return undefined
|
|
193
|
-
|
|
194
|
-
const prototype = Object.getPrototypeOf(current)
|
|
195
|
-
const isArray = Array.isArray(current)
|
|
196
|
-
if (
|
|
197
|
-
(isArray && prototype !== Array.prototype) ||
|
|
198
|
-
(!isArray && prototype !== Object.prototype && prototype !== null)
|
|
199
|
-
) {
|
|
200
|
-
return undefined
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// JSON.stringify invokes toJSON even when it is non-enumerable. Reject
|
|
204
|
-
// custom hooks on either the value or its otherwise-plain prototype.
|
|
205
|
-
if (
|
|
206
|
-
Object.getOwnPropertyDescriptor(current, 'toJSON') !== undefined ||
|
|
207
|
-
(prototype !== null && Object.getOwnPropertyDescriptor(prototype, 'toJSON') !== undefined)
|
|
208
|
-
) {
|
|
209
|
-
return undefined
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
active.add(current)
|
|
213
|
-
if (isArray) {
|
|
214
|
-
const array = current as unknown[]
|
|
215
|
-
const length = array.length
|
|
216
|
-
bytes++ // [
|
|
217
|
-
// Even the shortest JSON value takes one byte; account for commas
|
|
218
|
-
// and the closing bracket before traversing potentially huge arrays.
|
|
219
|
-
const minimumContentBytes = length === 0 ? 0 : length * 2 - 1
|
|
220
|
-
if (bytes + minimumContentBytes + 1 > limit) return undefined
|
|
221
|
-
stack.push({ kind: 'array', value: array, index: 0, length, depth: frame.depth })
|
|
222
|
-
} else {
|
|
223
|
-
bytes++ // {
|
|
224
|
-
stack.push({
|
|
225
|
-
kind: 'object',
|
|
226
|
-
value: current as Record<string, unknown>,
|
|
227
|
-
keys: ownEnumerableKeys(current as Record<string, unknown>),
|
|
228
|
-
first: true,
|
|
229
|
-
depth: frame.depth,
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} else if (frame.kind === 'array') {
|
|
234
|
-
if (frame.index === frame.length) {
|
|
235
|
-
bytes++ // ]
|
|
236
|
-
if (bytes > limit) return undefined
|
|
237
|
-
stack.pop()
|
|
238
|
-
active.delete(frame.value)
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (frame.index > 0) bytes++ // ,
|
|
243
|
-
const descriptor = Object.getOwnPropertyDescriptor(frame.value, String(frame.index))
|
|
244
|
-
if (
|
|
245
|
-
descriptor === undefined ||
|
|
246
|
-
!descriptor.enumerable ||
|
|
247
|
-
!Object.hasOwn(descriptor, 'value')
|
|
248
|
-
) {
|
|
249
|
-
return undefined
|
|
250
|
-
}
|
|
251
|
-
frame.index++
|
|
252
|
-
stack.push({ kind: 'value', value: descriptor.value, depth: frame.depth + 1 })
|
|
253
|
-
} else {
|
|
254
|
-
const next = frame.keys.next()
|
|
255
|
-
if (next.done) {
|
|
256
|
-
bytes++ // }
|
|
257
|
-
if (bytes > limit) return undefined
|
|
258
|
-
stack.pop()
|
|
259
|
-
active.delete(frame.value)
|
|
260
|
-
continue
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const key = next.value
|
|
264
|
-
const descriptor = Object.getOwnPropertyDescriptor(frame.value, key)
|
|
265
|
-
if (
|
|
266
|
-
descriptor === undefined ||
|
|
267
|
-
!descriptor.enumerable ||
|
|
268
|
-
!Object.hasOwn(descriptor, 'value')
|
|
269
|
-
) {
|
|
270
|
-
return undefined
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const keyBytes = serializedStringBytes(key, limit - bytes - (frame.first ? 1 : 2))
|
|
274
|
-
if (keyBytes === undefined) return undefined
|
|
275
|
-
bytes += (frame.first ? 0 : 1) + keyBytes + 1 // optional comma, key, colon
|
|
276
|
-
frame.first = false
|
|
277
|
-
stack.push({ kind: 'value', value: descriptor.value, depth: frame.depth + 1 })
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (bytes > limit) return undefined
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return bytes
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
287
|
-
if (
|
|
288
|
-
typeof value !== 'object' ||
|
|
289
|
-
value === null ||
|
|
290
|
-
Array.isArray(value) ||
|
|
291
|
-
utilTypes.isProxy(value)
|
|
292
|
-
) {
|
|
293
|
-
return false
|
|
294
|
-
}
|
|
295
|
-
const prototype = Object.getPrototypeOf(value)
|
|
296
|
-
return prototype === Object.prototype || prototype === null
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function maskMatch(match: string): string {
|
|
300
|
-
const characters = Array.from(match)
|
|
301
|
-
if (characters.length <= 8) return '****'
|
|
302
|
-
return `${characters.slice(0, 4).join('')}****${characters.slice(-4).join('')}`
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function isIdentifierCode(code: number | undefined): boolean {
|
|
306
|
-
return (
|
|
307
|
-
code !== undefined &&
|
|
308
|
-
((code >= 48 && code <= 57) ||
|
|
309
|
-
(code >= 65 && code <= 90) ||
|
|
310
|
-
(code >= 97 && code <= 122) ||
|
|
311
|
-
code === 95 ||
|
|
312
|
-
code === 45)
|
|
313
|
-
)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function skipAssignmentWhitespace(input: string, index: number): number {
|
|
317
|
-
while (index < input.length && input[index] !== '\n' && WHITESPACE_RE.test(input[index] ?? '')) {
|
|
318
|
-
index++
|
|
319
|
-
}
|
|
320
|
-
return index
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function findAssignmentMatch(
|
|
324
|
-
input: string,
|
|
325
|
-
lowerInput: string,
|
|
326
|
-
pattern: SecretPattern,
|
|
327
|
-
): string | undefined {
|
|
328
|
-
const marker =
|
|
329
|
-
pattern.name === 'api-key-assignment'
|
|
330
|
-
? 'api'
|
|
331
|
-
: pattern.name === 'password-assignment'
|
|
332
|
-
? 'password'
|
|
333
|
-
: 'secret'
|
|
334
|
-
if (!lowerInput.includes(marker)) return undefined
|
|
335
|
-
|
|
336
|
-
IDENTIFIER_TOKEN_RE.lastIndex = 0
|
|
337
|
-
let token = IDENTIFIER_TOKEN_RE.exec(input)
|
|
338
|
-
while (token !== null) {
|
|
339
|
-
if (pattern.regex.test(token[0])) {
|
|
340
|
-
let index = skipAssignmentWhitespace(input, token.index + token[0].length)
|
|
341
|
-
if (input[index] === ':' || input[index] === '=') {
|
|
342
|
-
index = skipAssignmentWhitespace(input, index + 1)
|
|
343
|
-
if (pattern.name === 'api-key-assignment') {
|
|
344
|
-
if (input[index] === '"' || input[index] === "'") index++
|
|
345
|
-
const valueStart = index
|
|
346
|
-
while (isIdentifierCode(input.charCodeAt(index))) index++
|
|
347
|
-
if (index - valueStart >= 16) {
|
|
348
|
-
if (input[index] === '"' || input[index] === "'") index++
|
|
349
|
-
return input.slice(token.index, index)
|
|
350
|
-
}
|
|
351
|
-
} else if (input[index] === '"' || input[index] === "'") {
|
|
352
|
-
index++
|
|
353
|
-
const valueStart = index
|
|
354
|
-
while (
|
|
355
|
-
index < input.length &&
|
|
356
|
-
input[index] !== '\n' &&
|
|
357
|
-
input[index] !== '"' &&
|
|
358
|
-
input[index] !== "'"
|
|
359
|
-
) {
|
|
360
|
-
index++
|
|
361
|
-
}
|
|
362
|
-
if (index - valueStart >= 8 && (input[index] === '"' || input[index] === "'")) {
|
|
363
|
-
return input.slice(token.index, index + 1)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
token = IDENTIFIER_TOKEN_RE.exec(input)
|
|
369
|
-
}
|
|
370
|
-
return undefined
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function findAsciiInsensitive(input: string, needle: string, from: number): number {
|
|
374
|
-
for (let index = from; index <= input.length - needle.length; index++) {
|
|
375
|
-
let matched = true
|
|
376
|
-
for (let offset = 0; offset < needle.length; offset++) {
|
|
377
|
-
const code = input.charCodeAt(index + offset)
|
|
378
|
-
const lowerCode = code >= 65 && code <= 90 ? code + 32 : code
|
|
379
|
-
if (lowerCode !== needle.charCodeAt(offset)) {
|
|
380
|
-
matched = false
|
|
381
|
-
break
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (matched) return index
|
|
385
|
-
}
|
|
386
|
-
return -1
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function startsWithEyJ(input: string, index: number): boolean {
|
|
390
|
-
if (index < 0 || index + 3 > input.length) return false
|
|
391
|
-
const e = input.charCodeAt(index)
|
|
392
|
-
const y = input.charCodeAt(index + 1)
|
|
393
|
-
const j = input.charCodeAt(index + 2)
|
|
394
|
-
return (e === 69 || e === 101) && (y === 89 || y === 121) && (j === 74 || j === 106)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function findJwtMatch(input: string): string | undefined {
|
|
398
|
-
let searchFrom = 0
|
|
399
|
-
while (searchFrom < input.length) {
|
|
400
|
-
const start = findAsciiInsensitive(input, 'eyj', searchFrom)
|
|
401
|
-
if (start < 0) return undefined
|
|
402
|
-
let firstDot = start + 3
|
|
403
|
-
while (isIdentifierCode(input.charCodeAt(firstDot))) firstDot++
|
|
404
|
-
if (input[firstDot] !== '.') {
|
|
405
|
-
// No later prefix inside this base64 segment can reach a period either.
|
|
406
|
-
searchFrom = firstDot + 1
|
|
407
|
-
continue
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const secondStart = firstDot + 1
|
|
411
|
-
if (!startsWithEyJ(input, secondStart)) {
|
|
412
|
-
searchFrom = secondStart
|
|
413
|
-
continue
|
|
414
|
-
}
|
|
415
|
-
let secondDot = secondStart + 3
|
|
416
|
-
while (isIdentifierCode(input.charCodeAt(secondDot))) secondDot++
|
|
417
|
-
if (input[secondDot] !== '.') {
|
|
418
|
-
searchFrom = secondDot + 1
|
|
419
|
-
continue
|
|
420
|
-
}
|
|
421
|
-
let end = secondDot + 1
|
|
422
|
-
while (isIdentifierCode(input.charCodeAt(end))) end++
|
|
423
|
-
return input.slice(start, end)
|
|
424
|
-
}
|
|
425
|
-
return undefined
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** Scan serialized tool text, returning no raw input or match values. */
|
|
429
|
-
export function scanSecretText(input: unknown): SecretScanResult {
|
|
430
|
-
if (typeof input !== 'string') return { status: 'invalid' }
|
|
431
|
-
// A code-unit precheck avoids doing a full byte count for obviously huge
|
|
432
|
-
// strings. UTF-8 byte size is authoritative for the supported-input limit.
|
|
433
|
-
if (input.length > MAX_SECRET_SCAN_INPUT_BYTES) return { status: 'invalid' }
|
|
434
|
-
if (Buffer.byteLength(input, 'utf8') > MAX_SECRET_SCAN_INPUT_BYTES) {
|
|
435
|
-
return { status: 'invalid' }
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const findings: SecretScanFinding[] = []
|
|
439
|
-
const lowerInput = input.toLowerCase()
|
|
440
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
441
|
-
const match =
|
|
442
|
-
pattern.name === 'jwt'
|
|
443
|
-
? findJwtMatch(input)
|
|
444
|
-
: pattern.confidence === 'low' && pattern.name !== 'bifrost-header'
|
|
445
|
-
? findAssignmentMatch(input, lowerInput, pattern)
|
|
446
|
-
: pattern.regex.exec(input)?.[0]
|
|
447
|
-
if (match !== undefined) {
|
|
448
|
-
findings.push({
|
|
449
|
-
confidence: pattern.confidence,
|
|
450
|
-
pattern: pattern.name,
|
|
451
|
-
masked: maskMatch(match),
|
|
452
|
-
})
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (findings.some((finding) => finding.confidence === 'high')) {
|
|
457
|
-
return { status: 'block', findings }
|
|
458
|
-
}
|
|
459
|
-
if (findings.length > 0) return { status: 'advisory', findings }
|
|
460
|
-
return { status: 'clean' }
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** Serialize the hook payload as the former stdin protocol did, then scan it. */
|
|
464
|
-
export function scanSecretPayload(payload: unknown): SecretScanResult {
|
|
465
|
-
try {
|
|
466
|
-
if (!isPlainObject(payload)) return { status: 'invalid' }
|
|
467
|
-
const toolInput = Object.getOwnPropertyDescriptor(payload, 'tool_input')
|
|
468
|
-
if (
|
|
469
|
-
toolInput === undefined ||
|
|
470
|
-
!toolInput.enumerable ||
|
|
471
|
-
!Object.hasOwn(toolInput, 'value') ||
|
|
472
|
-
!isPlainObject(toolInput.value)
|
|
473
|
-
) {
|
|
474
|
-
return { status: 'invalid' }
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const measuredBytes = measureJsonBytes(payload, MAX_SECRET_SCAN_INPUT_BYTES)
|
|
478
|
-
if (measuredBytes === undefined) return { status: 'invalid' }
|
|
479
|
-
|
|
480
|
-
const serialized = JSON.stringify(payload)
|
|
481
|
-
if (typeof serialized !== 'string' || Buffer.byteLength(serialized, 'utf8') !== measuredBytes) {
|
|
482
|
-
return { status: 'invalid' }
|
|
483
|
-
}
|
|
484
|
-
return scanSecretText(serialized)
|
|
485
|
-
} catch {
|
|
486
|
-
// Never expose serialization errors: hostile/custom values can include input.
|
|
487
|
-
return { status: 'invalid' }
|
|
488
|
-
}
|
|
489
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Pantheon TUI compatibility
|
|
2
|
-
|
|
3
|
-
- **OpenCode V1:** supported by the existing `@opencode-ai/plugin@1.18.33`
|
|
4
|
-
TUI API and its `sidebar_content` / `app_bottom` slots.
|
|
5
|
-
- **OpenCode V2 TUI:** unsupported. This package still exports the V1
|
|
6
|
-
`TuiPlugin` factory; it has no verified V2 TUI adapter. Do not infer the V2
|
|
7
|
-
TUI registration or slot contract from the V2 plugin `setup` contract. The
|
|
8
|
-
installed OpenCode host could not be analyzed with REA in this environment
|
|
9
|
-
because no native analysis provider was available, so a V2 contract must be
|
|
10
|
-
verified against a suitable shipped host artifact before adding an adapter.
|
|
11
|
-
|
|
12
|
-
The V1 UI and its slot registrations remain unchanged. Keep this package on its
|
|
13
|
-
existing V1 pins until a separately verified V2 migration can preserve the
|
|
14
|
-
legacy UI behavior or explicitly replace it.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: council-synthesis
|
|
3
|
-
description: "Structured multi-agent council for explicit /pantheon requests and material decisions needing distinct perspectives. Load only when a council is actually needed."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Council synthesis — `/pantheon`
|
|
7
|
-
|
|
8
|
-
Use only when the user invokes `/pantheon` or a material decision genuinely needs distinct specialist views. Ordinary implementation, explanation, and bounded fixes do not need a council.
|
|
9
|
-
|
|
10
|
-
## Run one useful round
|
|
11
|
-
|
|
12
|
-
1. Search `council_decisions` for a directly relevant precedent. Reuse a fresh, matching decision with a warning to re-check current context; otherwise treat it as context, not authority.
|
|
13
|
-
2. Select two specialists by default. Add a third only when the question spans a separate domain; include @themis when security, correctness, or a material quality gate requires review.
|
|
14
|
-
3. Ask each specialist the same focused question and request a position, evidence, trade-offs, key risks, and confidence. Dispatch independent work together only when native background delegation is enabled; otherwise use normal `task()` calls.
|
|
15
|
-
4. Synthesize after one response round. State agreements, real disagreements, missing responses, and uncertainty. Do not run a routine rebuttal, duplicate moderator/audit pass, or repeat the panel. Check a disputed factual claim only when it could change the recommendation; ask one focused follow-up if evidence remains insufficient.
|
|
16
|
-
5. Give the user a concise recommendation with the main trade-off and any unresolved risk. Cite external research when used. Preserve human approval and Themis gates for sensitive or irreversible actions.
|
|
17
|
-
6. Persist only a reusable, material decision in `council_decisions`; do not store routine opinions or user-specific details without a need.
|
|
18
|
-
|
|
19
|
-
## Specialist response
|
|
20
|
-
|
|
21
|
-
```text
|
|
22
|
-
position: one sentence answering the question
|
|
23
|
-
evidence: the key facts or reasoning (2–4 sentences)
|
|
24
|
-
trade-offs: one concrete gain and loss
|
|
25
|
-
risks: up to 3
|
|
26
|
-
confidence: High | Medium | Low
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Use High only with multiple verifiable facts, Medium with some evidence, and Low for mainly judgment-based conclusions. Do not invent consensus or confidence.
|