thinkpool-pair 0.7.363 → 0.7.365
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 +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6240
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -0
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -91
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -0
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +15 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-capabilities.json +11 -2
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/repo-search.mjs
CHANGED
|
@@ -1,190 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const MAX_QUERY_CHARS = 160
|
|
4
|
-
const DEFAULT_MAX_RESULTS = 8
|
|
5
|
-
const MAX_FILES_BUFFER = 16 * 1024 * 1024
|
|
6
|
-
|
|
7
|
-
const STOP_WORDS = new Set([
|
|
8
|
-
'a', 'an', 'and', 'every', 'file', 'files', 'find', 'for', 'from', 'in',
|
|
9
|
-
'inside', 'is', 'locate', 'me', 'of', 'please', 'show', 'the', 'to', 'under',
|
|
10
|
-
'where', 'with',
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
const EXPANSIONS = Object.freeze({
|
|
14
|
-
api: ['endpoint', 'route', 'server'],
|
|
15
|
-
auth: ['login', 'session', 'oauth'],
|
|
16
|
-
component: ['card', 'panel', 'view', 'widget'],
|
|
17
|
-
config: ['configuration', 'settings'],
|
|
18
|
-
database: ['db', 'sql', 'schema'],
|
|
19
|
-
image: ['asset', 'gallery', 'media', 'photo', 'picture', 'thumbnail'],
|
|
20
|
-
library: ['catalog', 'collection', 'index', 'lib'],
|
|
21
|
-
login: ['auth', 'oauth', 'session'],
|
|
22
|
-
session: ['auth', 'login'],
|
|
23
|
-
style: ['css', 'theme'],
|
|
24
|
-
styles: ['css', 'theme'],
|
|
25
|
-
test: ['spec'],
|
|
26
|
-
tests: ['spec'],
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const cleanQuery = (value) => String(value || '')
|
|
30
|
-
// Deliberately strip ASCII control characters from room-supplied queries.
|
|
31
|
-
// eslint-disable-next-line no-control-regex
|
|
32
|
-
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
33
|
-
.replace(/\s+/g, ' ')
|
|
34
|
-
.trim()
|
|
35
|
-
|
|
36
|
-
const unique = (values) => [...new Set(values.filter(Boolean))]
|
|
37
|
-
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
38
|
-
const normalizePath = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
39
|
-
const wordish = (value) => String(value || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim()
|
|
40
|
-
const pathContainsSegment = (path, segment) => `/${normalizePath(path).toLowerCase()}/`.includes(`/${segment.toLowerCase().replace(/^\/+|\/+$/g, '')}/`)
|
|
41
|
-
|
|
42
|
-
function queryTokens(query) {
|
|
43
|
-
return unique((query.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_-]*/gu) || [])
|
|
44
|
-
.map((token) => token.replace(/^[-_]+|[-_]+$/g, ''))
|
|
45
|
-
.filter((token) => token && !STOP_WORDS.has(token)))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function locationHints(query) {
|
|
49
|
-
return unique([...query.toLowerCase().matchAll(/\b(?:from|in|inside|under)\s+([\p{L}\p{N}_.\/-]+)/gu)]
|
|
50
|
-
.map((match) => match[1].replace(/^\/+|\/+$/g, ''))
|
|
51
|
-
.filter(Boolean))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function defaultRun(command, args, cwd) {
|
|
55
|
-
return execFileSync(command, args, {
|
|
56
|
-
cwd,
|
|
57
|
-
encoding: 'utf8',
|
|
58
|
-
timeout: command === 'rg' ? 1400 : 3000,
|
|
59
|
-
maxBuffer: MAX_FILES_BUFFER,
|
|
60
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function listedFiles(run, cwd) {
|
|
65
|
-
const raw = String(run('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd) || '')
|
|
66
|
-
return unique(raw.split('\0').map(normalizePath).filter(Boolean))
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function contentMatches(run, cwd, terms, knownFiles) {
|
|
70
|
-
const matches = new Map()
|
|
71
|
-
if (!terms.length) return matches
|
|
72
|
-
const pattern = terms.slice().sort((a, b) => b.length - a.length).map(escapeRegex).join('|')
|
|
73
|
-
let raw = ''
|
|
74
|
-
try {
|
|
75
|
-
raw = String(run('rg', [
|
|
76
|
-
'--json',
|
|
77
|
-
'--ignore-case',
|
|
78
|
-
'--max-count', '4',
|
|
79
|
-
'--no-messages',
|
|
80
|
-
'--regexp', pattern,
|
|
81
|
-
'.',
|
|
82
|
-
], cwd) || '')
|
|
83
|
-
} catch {
|
|
84
|
-
// `rg` is the fast path. A host without it still gets filename/path search;
|
|
85
|
-
// repository search must never block the room or fall through to an LLM turn.
|
|
86
|
-
return matches
|
|
87
|
-
}
|
|
88
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
89
|
-
if (!line) continue
|
|
90
|
-
let record
|
|
91
|
-
try { record = JSON.parse(line) } catch { continue }
|
|
92
|
-
if (record?.type !== 'match') continue
|
|
93
|
-
const path = normalizePath(record.data?.path?.text)
|
|
94
|
-
if (!knownFiles.has(path)) continue
|
|
95
|
-
const lineNumber = Number(record.data?.line_number)
|
|
96
|
-
const text = String(record.data?.lines?.text || '').toLowerCase()
|
|
97
|
-
const prior = matches.get(path)
|
|
98
|
-
if (!prior) matches.set(path, { line: Number.isFinite(lineNumber) ? lineNumber : null, text })
|
|
99
|
-
else prior.text += ` ${text}`
|
|
100
|
-
}
|
|
101
|
-
return matches
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function candidateScore(path, original, expanded, content) {
|
|
105
|
-
const lowerPath = path.toLowerCase()
|
|
106
|
-
const base = lowerPath.split('/').at(-1) || lowerPath
|
|
107
|
-
const pathWords = ` ${wordish(lowerPath)} `
|
|
108
|
-
const contentText = content?.text || ''
|
|
109
|
-
let score = 0
|
|
110
|
-
|
|
111
|
-
const phrase = original.join(' ')
|
|
112
|
-
if (phrase && pathWords.includes(` ${phrase} `)) score += 100
|
|
113
|
-
for (const token of original) {
|
|
114
|
-
if (base.includes(token)) score += 30
|
|
115
|
-
if (lowerPath.includes(token)) score += 16
|
|
116
|
-
if (pathWords.includes(` ${token} `)) score += 8
|
|
117
|
-
if (contentText.includes(token)) score += 8
|
|
118
|
-
}
|
|
119
|
-
for (const token of expanded) {
|
|
120
|
-
if (base.includes(token)) score += 8
|
|
121
|
-
else if (lowerPath.includes(token)) score += 5
|
|
122
|
-
if (contentText.includes(token)) score += 2
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const rootMatches = (haystack) => original.filter((token) =>
|
|
126
|
-
haystack.includes(token) || (EXPANSIONS[token] || []).some((alias) => haystack.includes(alias)))
|
|
127
|
-
const pathCoverage = rootMatches(lowerPath).length
|
|
128
|
-
const coverage = rootMatches(`${lowerPath} ${contentText}`).length
|
|
129
|
-
score += (coverage ** 2) * 18 + (pathCoverage ** 2) * 22
|
|
130
|
-
|
|
131
|
-
const ext = base.includes('.') ? base.slice(base.lastIndexOf('.')) : ''
|
|
132
|
-
const componentFile = ['.jsx', '.tsx', '.vue', '.svelte'].includes(ext)
|
|
133
|
-
if (original.includes('component') && componentFile) score += 60
|
|
134
|
-
if (original.some((token) => token === 'test' || token === 'tests') && /(?:^|\/)(?:test|tests|spec|specs)(?:\/|$)|\.(?:test|spec)\./.test(lowerPath)) score += 10
|
|
135
|
-
if (original.some((token) => token === 'asset' || token === 'assets') && /(?:^|\/)assets?(?:\/|$)/.test(lowerPath)) score += 10
|
|
136
|
-
const testPath = /(?:^|\/)(?:test|tests|spec|specs)(?:\/|$)|\.(?:test|spec)\./.test(lowerPath)
|
|
137
|
-
if (testPath && !original.some((token) => token === 'test' || token === 'tests')) score -= 25
|
|
138
|
-
if (/^(?:docs|marketing|archive|\.claude)\//.test(lowerPath)) score -= 35
|
|
139
|
-
if (/^(?:src|app|lib|api|bridge|server|client|components|packages)\//.test(lowerPath)) score += 12
|
|
140
|
-
return { score, coverage, pathCoverage, componentFile }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function repoSearch({
|
|
144
|
-
cwd = process.cwd(),
|
|
145
|
-
query,
|
|
146
|
-
maxResults = DEFAULT_MAX_RESULTS,
|
|
147
|
-
run = defaultRun,
|
|
148
|
-
now = () => Date.now(),
|
|
149
|
-
} = {}) {
|
|
150
|
-
const startedAt = now()
|
|
151
|
-
const cleaned = cleanQuery(query)
|
|
152
|
-
const finish = (result) => ({ ...result, durationMs: Math.max(0, Math.round(now() - startedAt)) })
|
|
153
|
-
|
|
154
|
-
if (!cleaned) return finish({ query: '', results: [], total: 0, truncated: false, error: 'Use /find followed by a filename, symbol, or code concept.' })
|
|
155
|
-
if (cleaned.length > MAX_QUERY_CHARS) return finish({ query: cleaned.slice(0, MAX_QUERY_CHARS), results: [], total: 0, truncated: false, error: `Keep /find queries under ${MAX_QUERY_CHARS} characters.` })
|
|
156
|
-
|
|
157
|
-
const original = queryTokens(cleaned)
|
|
158
|
-
if (!original.length) return finish({ query: cleaned, results: [], total: 0, truncated: false, error: 'Try a filename, symbol, or code concept.' })
|
|
159
|
-
const expanded = unique(original.flatMap((token) => EXPANSIONS[token] || []))
|
|
160
|
-
|
|
161
|
-
let files
|
|
162
|
-
try { files = listedFiles(run, cwd) } catch {
|
|
163
|
-
return finish({ query: cleaned, results: [], total: 0, truncated: false, error: 'Repository search is unavailable outside a readable Git checkout.' })
|
|
164
|
-
}
|
|
165
|
-
const activeLocations = locationHints(cleaned).filter((hint) => files.some((path) => pathContainsSegment(path, hint)))
|
|
166
|
-
const eligibleFiles = activeLocations.length
|
|
167
|
-
? files.filter((path) => activeLocations.every((hint) => pathContainsSegment(path, hint)))
|
|
168
|
-
: files
|
|
169
|
-
const knownFiles = new Set(eligibleFiles)
|
|
170
|
-
const matches = contentMatches(run, cwd, unique([...original, ...expanded]), knownFiles)
|
|
171
|
-
const minimumCoverage = Math.max(1, Math.ceil(original.length * 0.6))
|
|
172
|
-
const ranked = eligibleFiles
|
|
173
|
-
.map((path) => {
|
|
174
|
-
const rank = candidateScore(path, original, expanded, matches.get(path))
|
|
175
|
-
return { path, line: matches.get(path)?.line || null, ...rank }
|
|
176
|
-
})
|
|
177
|
-
.filter((item) => item.score > 0 && (
|
|
178
|
-
item.coverage >= minimumCoverage
|
|
179
|
-
|| (original.includes('component') && item.componentFile && item.coverage >= 2)
|
|
180
|
-
))
|
|
181
|
-
.sort((a, b) => b.score - a.score || b.pathCoverage - a.pathCoverage || a.path.length - b.path.length || a.path.localeCompare(b.path))
|
|
182
|
-
|
|
183
|
-
const limit = Math.max(1, Math.min(20, Number(maxResults) || DEFAULT_MAX_RESULTS))
|
|
184
|
-
return finish({
|
|
185
|
-
query: cleaned,
|
|
186
|
-
results: ranked.slice(0, limit).map(({ path, line }) => ({ path, ...(line ? { line } : {}) })),
|
|
187
|
-
total: ranked.length,
|
|
188
|
-
truncated: ranked.length > limit,
|
|
189
|
-
})
|
|
190
|
-
}
|
|
1
|
+
import{execFileSync as e}from"node:child_process";const t=new Set(["a","an","and","every","file","files","find","for","from","in","inside","is","locate","me","of","please","show","the","to","under","where","with"]),s=Object.freeze({api:["endpoint","route","server"],auth:["login","session","oauth"],component:["card","panel","view","widget"],config:["configuration","settings"],database:["db","sql","schema"],image:["asset","gallery","media","photo","picture","thumbnail"],library:["catalog","collection","index","lib"],login:["auth","oauth","session"],session:["auth","login"],style:["css","theme"],styles:["css","theme"],test:["spec"],tests:["spec"]}),n=e=>[...new Set(e.filter(Boolean))],r=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=e=>String(e||"").replace(/\\/g,"/").replace(/^\.\//,""),a=(e,t)=>`/${o(e).toLowerCase()}/`.includes(`/${t.toLowerCase().replace(/^\/+|\/+$/g,"")}/`);function c(t,s,n){return e(t,s,{cwd:n,encoding:"utf8",timeout:"rg"===t?1400:3e3,maxBuffer:16777216,stdio:["ignore","pipe","pipe"]})}export function repoSearch({cwd:e=process.cwd(),query:i,maxResults:l=8,run:u=c,now:p=()=>Date.now()}={}){const d=p(),h=String(i||"").replace(/[\u0000-\u001f\u007f]/g," ").replace(/\s+/g," ").trim(),g=e=>({...e,durationMs:Math.max(0,Math.round(p()-d))});if(!h)return g({query:"",results:[],total:0,truncated:!1,error:"Use /find followed by a filename, symbol, or code concept."});if(h.length>160)return g({query:h.slice(0,160),results:[],total:0,truncated:!1,error:"Keep /find queries under 160 characters."});const f=function(e){return n((e.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_-]*/gu)||[]).map(e=>e.replace(/^[-_]+|[-_]+$/g,"")).filter(e=>e&&!t.has(e)))}(h);if(!f.length)return g({query:h,results:[],total:0,truncated:!1,error:"Try a filename, symbol, or code concept."});const m=n(f.flatMap(e=>s[e]||[]));let w;try{w=function(e,t){const s=String(e("git",["ls-files","-z","--cached","--others","--exclude-standard"],t)||"");return n(s.split("\0").map(o).filter(Boolean))}(u,e)}catch{return g({query:h,results:[],total:0,truncated:!1,error:"Repository search is unavailable outside a readable Git checkout."})}const y=function(e){return n([...e.toLowerCase().matchAll(/\b(?:from|in|inside|under)\s+([\p{L}\p{N}_.\/-]+)/gu)].map(e=>e[1].replace(/^\/+|\/+$/g,"")).filter(Boolean))}(h).filter(e=>w.some(t=>a(t,e))),b=y.length?w.filter(e=>y.every(t=>a(e,t))):w,x=new Set(b),v=function(e,t,s,n){const a=new Map;if(!s.length)return a;const c=s.slice().sort((e,t)=>t.length-e.length).map(r).join("|");let i="";try{i=String(e("rg",["--json","--ignore-case","--max-count","4","--no-messages","--regexp",c,"."],t)||"")}catch{return a}for(const e of i.split(/\r?\n/)){if(!e)continue;let t;try{t=JSON.parse(e)}catch{continue}if("match"!==t?.type)continue;const s=o(t.data?.path?.text);if(!n.has(s))continue;const r=Number(t.data?.line_number),c=String(t.data?.lines?.text||"").toLowerCase(),i=a.get(s);i?i.text+=` ${c}`:a.set(s,{line:Number.isFinite(r)?r:null,text:c})}return a}(u,e,n([...f,...m]),x),$=Math.max(1,Math.ceil(.6*f.length)),C=b.map(e=>{const t=function(e,t,n,r){const o=e.toLowerCase(),a=o.split("/").at(-1)||o,c=` ${i=o,String(i||"").toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim()} `;var i;const l=r?.text||"";let u=0;const p=t.join(" ");p&&c.includes(` ${p} `)&&(u+=100);for(const e of t)a.includes(e)&&(u+=30),o.includes(e)&&(u+=16),c.includes(` ${e} `)&&(u+=8),l.includes(e)&&(u+=8);for(const e of n)a.includes(e)?u+=8:o.includes(e)&&(u+=5),l.includes(e)&&(u+=2);const d=e=>t.filter(t=>e.includes(t)||(s[t]||[]).some(t=>e.includes(t))),h=d(o).length,g=d(`${o} ${l}`).length;u+=g**2*18+h**2*22;const f=a.includes(".")?a.slice(a.lastIndexOf(".")):"",m=[".jsx",".tsx",".vue",".svelte"].includes(f);return t.includes("component")&&m&&(u+=60),t.some(e=>"test"===e||"tests"===e)&&/(?:^|\/)(?:test|tests|spec|specs)(?:\/|$)|\.(?:test|spec)\./.test(o)&&(u+=10),t.some(e=>"asset"===e||"assets"===e)&&/(?:^|\/)assets?(?:\/|$)/.test(o)&&(u+=10),/(?:^|\/)(?:test|tests|spec|specs)(?:\/|$)|\.(?:test|spec)\./.test(o)&&!t.some(e=>"test"===e||"tests"===e)&&(u-=25),/^(?:docs|marketing|archive|\.claude)\//.test(o)&&(u-=35),/^(?:src|app|lib|api|bridge|server|client|components|packages)\//.test(o)&&(u+=12),{score:u,coverage:g,pathCoverage:h,componentFile:m}}(e,f,m,v.get(e));return{path:e,line:v.get(e)?.line||null,...t}}).filter(e=>e.score>0&&(e.coverage>=$||f.includes("component")&&e.componentFile&&e.coverage>=2)).sort((e,t)=>t.score-e.score||t.pathCoverage-e.pathCoverage||e.path.length-t.path.length||e.path.localeCompare(t.path)),L=Math.max(1,Math.min(20,Number(l)||8));return g({query:h,results:C.slice(0,L).map(({path:e,line:t})=>({path:e,...t?{line:t}:{}})),total:C.length,truncated:C.length>L})}
|
package/review-check.mjs
CHANGED
|
@@ -1,182 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// This is host execution parity with Claude/Codex Flow, not a malicious-code sandbox.
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import fsp from 'node:fs/promises'
|
|
5
|
-
import os from 'node:os'
|
|
6
|
-
import path from 'node:path'
|
|
7
|
-
import { spawn } from 'node:child_process'
|
|
8
|
-
import { execFile } from 'node:child_process'
|
|
9
|
-
import { promisify } from 'node:util'
|
|
10
|
-
|
|
11
|
-
const execFileAsync = promisify(execFile)
|
|
12
|
-
|
|
13
|
-
const CHECKS = Object.freeze({
|
|
14
|
-
'node-test': ['node', ['--test']], 'npm-test': ['npm', ['test']],
|
|
15
|
-
'npm-build': ['npm', ['run', 'build']], pytest: ['pytest', []],
|
|
16
|
-
'cargo-test': ['cargo', ['test']], 'go-test': ['go', ['test', './...']],
|
|
17
|
-
})
|
|
18
|
-
const OUTPUT_CAP = 512 * 1024
|
|
19
|
-
|
|
20
|
-
// Codex's native read-only/review mode never raises approval cards. Mark only
|
|
21
|
-
// the pinned Git-blob reader as approval-free: it cannot execute target code,
|
|
22
|
-
// mutate the target, or reach the open world.
|
|
23
|
-
export const IMMUTABLE_REVIEW_READ_TOOL_EXTRAS = Object.freeze({
|
|
24
|
-
annotations: Object.freeze({
|
|
25
|
-
readOnlyHint: true,
|
|
26
|
-
destructiveHint: false,
|
|
27
|
-
idempotentHint: true,
|
|
28
|
-
openWorldHint: false,
|
|
29
|
-
}),
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// Fixed check ids prevent command injection, but the selected check still runs
|
|
33
|
-
// target-defined code. The scratch archive protects the reviewed worktree; it
|
|
34
|
-
// is not an OS filesystem/network sandbox. Advertise that boundary honestly so
|
|
35
|
-
// approvalPolicy=never reviewers refuse execution instead of silently granting
|
|
36
|
-
// host/open-world authority. Sandboxed runners can still expose this behind an
|
|
37
|
-
// explicit approval in runtimes that support one.
|
|
38
|
-
export const UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS = Object.freeze({
|
|
39
|
-
annotations: Object.freeze({
|
|
40
|
-
readOnlyHint: false,
|
|
41
|
-
destructiveHint: true,
|
|
42
|
-
idempotentHint: false,
|
|
43
|
-
openWorldHint: true,
|
|
44
|
-
}),
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
export function reviewCheckCommand(checkId) {
|
|
48
|
-
if (!Object.hasOwn(CHECKS, checkId)) throw new Error('unapproved review check')
|
|
49
|
-
const [command, args] = CHECKS[checkId]
|
|
50
|
-
return { command, args: [...args] }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function reviewCheckEnv(env = process.env, root = null) {
|
|
54
|
-
root ||= await fsp.mkdtemp(path.join(os.tmpdir(), 'tp-review-env-'))
|
|
55
|
-
const keptPath = env.PATH || process.env.PATH || ''
|
|
56
|
-
const home = path.join(root, 'home')
|
|
57
|
-
const npmCache = path.join(root, 'npm-cache')
|
|
58
|
-
const tmp = path.join(root, 'tmp')
|
|
59
|
-
await Promise.all([fsp.mkdir(home, { recursive: true }), fsp.mkdir(npmCache, { recursive: true }), fsp.mkdir(tmp, { recursive: true })])
|
|
60
|
-
return { PATH: keptPath, HOME: home, TMPDIR: path.join(root, 'tmp'), npm_config_cache: npmCache, npm_config_userconfig: path.join(home, '.npmrc'), GIT_CONFIG_NOSYSTEM: '1' }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function validateReviewTarget(target, snapshots) {
|
|
64
|
-
if (typeof target !== 'string' || !/^[a-z0-9-]{1,64}$/.test(target)) throw new Error('invalid reviewed target')
|
|
65
|
-
const snapshot = (Array.isArray(snapshots) ? snapshots : []).find((item) => item?.taskKey === target)
|
|
66
|
-
if (!snapshot || !/^[0-9a-f]{40}$/i.test(snapshot.sha) || !path.isAbsolute(snapshot.cwd || '')) throw new Error('reviewed target is not pinned')
|
|
67
|
-
return snapshot
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function validateReviewPath(filePath) {
|
|
71
|
-
if (typeof filePath !== 'string' || !filePath || filePath.length > 512 || path.posix.isAbsolute(filePath)) throw new Error('invalid review path')
|
|
72
|
-
const normalized = path.posix.normalize(filePath)
|
|
73
|
-
if (normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.split('/').some((part) => !part || part === '.')) throw new Error('invalid review path')
|
|
74
|
-
return normalized
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Read the pinned Git blob, never the reviewer worktree. git ls-tree refuses
|
|
78
|
-
// directories and symlinks before git show is allowed to materialize bytes.
|
|
79
|
-
export async function readReviewFile({ target, filePath, snapshots, maxBytes = 512 * 1024 } = {}) {
|
|
80
|
-
const snapshot = validateReviewTarget(target, snapshots)
|
|
81
|
-
const normalized = validateReviewPath(filePath)
|
|
82
|
-
const cap = Number.isInteger(maxBytes) && maxBytes > 0 && maxBytes <= 512 * 1024 ? maxBytes : 512 * 1024
|
|
83
|
-
const listed = await execFileAsync('git', ['-C', snapshot.cwd, 'ls-tree', '-z', snapshot.sha, '--', normalized], { encoding: 'buffer', maxBuffer: 1024 * 1024 })
|
|
84
|
-
const record = Buffer.from(listed.stdout).toString('utf8').split('\0').filter(Boolean)[0] || ''
|
|
85
|
-
const match = /^(100[0-7]{3}) blob ([0-9a-f]{40})\t(.+)$/.exec(record)
|
|
86
|
-
if (!match || match[3] !== normalized) throw new Error('review file is not a pinned regular file')
|
|
87
|
-
const shown = await execFileAsync('git', ['-C', snapshot.cwd, 'show', `${snapshot.sha}:${normalized}`], { encoding: 'buffer', maxBuffer: cap + 1 })
|
|
88
|
-
const bytes = Buffer.from(shown.stdout)
|
|
89
|
-
if (bytes.length > cap) throw new Error('review file exceeds byte limit')
|
|
90
|
-
return { target, sha: snapshot.sha, path: normalized, content: bytes.toString('utf8'), bytes: bytes.length }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function boundedPush(parts, value) {
|
|
94
|
-
const text = String(value || '')
|
|
95
|
-
parts.push(text)
|
|
96
|
-
let total = parts.reduce((n, part) => n + part.length, 0)
|
|
97
|
-
while (total > OUTPUT_CAP && parts.length) total -= parts.shift().length
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function killTree(child, signal = 'SIGTERM', { platform = process.platform, spawnImpl = spawn, taskkillTimeoutMs = 1_000 } = {}) {
|
|
101
|
-
if (!child?.pid) return true
|
|
102
|
-
if (platform === 'win32' && signal === 'SIGKILL') {
|
|
103
|
-
// Windows has no process-group equivalent. This is trusted-host parity,
|
|
104
|
-
// not unescapable process-tree isolation. taskkill itself can hang on a
|
|
105
|
-
// hostile/broken host, so race it: cleanup remains best-effort and the
|
|
106
|
-
// review request always settles.
|
|
107
|
-
return await new Promise((resolve) => {
|
|
108
|
-
let done = false
|
|
109
|
-
let timer = null
|
|
110
|
-
const finish = (ok) => {
|
|
111
|
-
if (done) return
|
|
112
|
-
done = true
|
|
113
|
-
if (timer) clearTimeout(timer)
|
|
114
|
-
resolve(ok)
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const taskkill = spawnImpl('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })
|
|
118
|
-
taskkill.once('close', (code) => finish(code === 0))
|
|
119
|
-
taskkill.once('error', () => finish(false))
|
|
120
|
-
timer = setTimeout(() => { try { taskkill.kill?.() } catch {} finish(false) }, taskkillTimeoutMs)
|
|
121
|
-
} catch { finish(false) }
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
try { platform === 'win32' ? child.kill(signal) : process.kill(-child.pid, signal); return true } catch { try { return child.kill(signal) } catch { return false } }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function runProcess(command, args, { cwd, env, timeoutMs = 120_000, killGraceMs = 250 } = {}) {
|
|
128
|
-
return new Promise((resolve) => {
|
|
129
|
-
const stdout = [], stderr = []
|
|
130
|
-
let timedOut = false
|
|
131
|
-
let settled = false
|
|
132
|
-
let child
|
|
133
|
-
let timer = null
|
|
134
|
-
let forceTimer = null
|
|
135
|
-
let settleTimer = null
|
|
136
|
-
const settle = (code) => {
|
|
137
|
-
if (settled) return
|
|
138
|
-
settled = true
|
|
139
|
-
if (timer) clearTimeout(timer)
|
|
140
|
-
if (forceTimer) clearTimeout(forceTimer)
|
|
141
|
-
if (settleTimer) clearTimeout(settleTimer)
|
|
142
|
-
resolve({ exitCode: Number.isInteger(code) ? code : null, stdout: stdout.join('').slice(-OUTPUT_CAP), stderr: stderr.join('').slice(-OUTPUT_CAP), timedOut })
|
|
143
|
-
}
|
|
144
|
-
try { child = spawn(command, args, { cwd, env, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }) }
|
|
145
|
-
catch (error) { stderr.push(String(error?.message || error)); settle(null); return }
|
|
146
|
-
child.stdout.on('data', (value) => boundedPush(stdout, value))
|
|
147
|
-
child.stderr.on('data', (value) => boundedPush(stderr, value))
|
|
148
|
-
timer = setTimeout(() => {
|
|
149
|
-
timedOut = true; void killTree(child)
|
|
150
|
-
forceTimer = setTimeout(async () => {
|
|
151
|
-
if (!await killTree(child, 'SIGKILL')) boundedPush(stderr, 'review check cleanup could not confirm process-tree termination')
|
|
152
|
-
// SIGKILL is unconditional on POSIX, but a platform/process failure must
|
|
153
|
-
// not leave the MCP call pending forever. A second best-effort kill then
|
|
154
|
-
// settles the caller; the scratch finally block can run.
|
|
155
|
-
settleTimer = setTimeout(() => {
|
|
156
|
-
void killTree(child, 'SIGKILL')
|
|
157
|
-
boundedPush(stderr, 'review check process did not report close after forced termination')
|
|
158
|
-
settle(null)
|
|
159
|
-
}, 1_000)
|
|
160
|
-
}, killGraceMs)
|
|
161
|
-
}, timeoutMs)
|
|
162
|
-
child.once('error', (error) => boundedPush(stderr, error?.message || error))
|
|
163
|
-
child.once('close', (code) => settle(code))
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function runReviewCheck({ target, checkId, snapshots, env = process.env, tmpdir = os.tmpdir(), timeoutMs } = {}) {
|
|
168
|
-
const snapshot = validateReviewTarget(target, snapshots)
|
|
169
|
-
const archiveDir = await fsp.mkdtemp(path.join(tmpdir, 'tp-review-'))
|
|
170
|
-
const archive = path.join(archiveDir, 'source.tar')
|
|
171
|
-
const scratch = path.join(archiveDir, 'tree')
|
|
172
|
-
try {
|
|
173
|
-
await fsp.mkdir(scratch)
|
|
174
|
-
const archiveResult = await runProcess('git', ['-C', snapshot.cwd, 'archive', '--format=tar', `--output=${archive}`, snapshot.sha], { env: await reviewCheckEnv(env, archiveDir), timeoutMs: 15_000 })
|
|
175
|
-
if (archiveResult.exitCode !== 0 || archiveResult.timedOut) return { pass: false, target, sha: snapshot.sha, command: ['git', 'archive'], exitCode: archiveResult.exitCode, ...archiveResult }
|
|
176
|
-
const extract = await runProcess('tar', ['-xf', archive, '-C', scratch], { env: await reviewCheckEnv(env, archiveDir), timeoutMs: 15_000 })
|
|
177
|
-
if (extract.exitCode !== 0 || extract.timedOut) return { pass: false, target, sha: snapshot.sha, command: ['tar', '-xf'], exitCode: extract.exitCode, ...extract }
|
|
178
|
-
const spec = reviewCheckCommand(checkId)
|
|
179
|
-
const result = await runProcess(spec.command, spec.args, { cwd: scratch, env: await reviewCheckEnv(env, archiveDir), timeoutMs })
|
|
180
|
-
return { pass: result.exitCode === 0 && !result.timedOut, target, sha: snapshot.sha, command: [spec.command, ...spec.args], ...result }
|
|
181
|
-
} finally { await fsp.rm(archiveDir, { recursive: true, force: true }) }
|
|
182
|
-
}
|
|
1
|
+
import e from"node:fs";import t from"node:fs/promises";import r from"node:os";import i from"node:path";import{spawn as o}from"node:child_process";import{execFile as n}from"node:child_process";import{promisify as s}from"node:util";const a=s(n),c=Object.freeze({"node-test":["node",["--test"]],"npm-test":["npm",["test"]],"npm-build":["npm",["run","build"]],pytest:["pytest",[]],"cargo-test":["cargo",["test"]],"go-test":["go",["test","./..."]]});export const IMMUTABLE_REVIEW_READ_TOOL_EXTRAS=Object.freeze({annotations:Object.freeze({readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1})});export const UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS=Object.freeze({annotations:Object.freeze({readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0})});export function reviewCheckCommand(e){if(!Object.hasOwn(c,e))throw new Error("unapproved review check");const[t,r]=c[e];return{command:t,args:[...r]}}export async function reviewCheckEnv(e=process.env,o=null){o||=await t.mkdtemp(i.join(r.tmpdir(),"tp-review-env-"));const n=e.PATH||process.env.PATH||"",s=i.join(o,"home"),a=i.join(o,"npm-cache"),c=i.join(o,"tmp");return await Promise.all([t.mkdir(s,{recursive:!0}),t.mkdir(a,{recursive:!0}),t.mkdir(c,{recursive:!0})]),{PATH:n,HOME:s,TMPDIR:i.join(o,"tmp"),npm_config_cache:a,npm_config_userconfig:i.join(s,".npmrc"),GIT_CONFIG_NOSYSTEM:"1"}}export function validateReviewTarget(e,t){if("string"!=typeof e||!/^[a-z0-9-]{1,64}$/.test(e))throw new Error("invalid reviewed target");const r=(Array.isArray(t)?t:[]).find(t=>t?.taskKey===e);if(!r||!/^[0-9a-f]{40}$/i.test(r.sha)||!i.isAbsolute(r.cwd||""))throw new Error("reviewed target is not pinned");return r}export function validateReviewPath(e){if("string"!=typeof e||!e||e.length>512||i.posix.isAbsolute(e))throw new Error("invalid review path");const t=i.posix.normalize(e);if("."===t||".."===t||t.startsWith("../")||t.split("/").some(e=>!e||"."===e))throw new Error("invalid review path");return t}export async function readReviewFile({target:e,filePath:t,snapshots:r,maxBytes:i=524288}={}){const o=validateReviewTarget(e,r),n=validateReviewPath(t),s=Number.isInteger(i)&&i>0&&i<=524288?i:524288,c=await a("git",["-C",o.cwd,"ls-tree","-z",o.sha,"--",n],{encoding:"buffer",maxBuffer:1048576}),d=Buffer.from(c.stdout).toString("utf8").split("\0").filter(Boolean)[0]||"",l=/^(100[0-7]{3}) blob ([0-9a-f]{40})\t(.+)$/.exec(d);if(!l||l[3]!==n)throw new Error("review file is not a pinned regular file");const m=await a("git",["-C",o.cwd,"show",`${o.sha}:${n}`],{encoding:"buffer",maxBuffer:s+1}),u=Buffer.from(m.stdout);if(u.length>s)throw new Error("review file exceeds byte limit");return{target:e,sha:o.sha,path:n,content:u.toString("utf8"),bytes:u.length}}function d(e,t){const r=String(t||"");e.push(r);let i=e.reduce((e,t)=>e+t.length,0);for(;i>524288&&e.length;)i-=e.shift().length}export async function killTree(e,t="SIGTERM",{platform:r=process.platform,spawnImpl:i=o,taskkillTimeoutMs:n=1e3}={}){if(!e?.pid)return!0;if("win32"===r&&"SIGKILL"===t)return await new Promise(t=>{let r=!1,o=null;const s=e=>{r||(r=!0,o&&clearTimeout(o),t(e))};try{const t=i("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore",windowsHide:!0});t.once("close",e=>s(0===e)),t.once("error",()=>s(!1)),o=setTimeout(()=>{try{t.kill?.()}catch{}s(!1)},n)}catch{s(!1)}});try{return"win32"===r?e.kill(t):process.kill(-e.pid,t),!0}catch{try{return e.kill(t)}catch{return!1}}}export function runProcess(e,t,{cwd:r,env:i,timeoutMs:n=12e4,killGraceMs:s=250}={}){return new Promise(a=>{const c=[],l=[];let m,u=!1,p=!1,f=null,w=null,h=null;const v=e=>{p||(p=!0,f&&clearTimeout(f),w&&clearTimeout(w),h&&clearTimeout(h),a({exitCode:Number.isInteger(e)?e:null,stdout:c.join("").slice(-524288),stderr:l.join("").slice(-524288),timedOut:u}))};try{m=o(e,t,{cwd:r,env:i,detached:"win32"!==process.platform,stdio:["ignore","pipe","pipe"]})}catch(e){return l.push(String(e?.message||e)),void v(null)}m.stdout.on("data",e=>d(c,e)),m.stderr.on("data",e=>d(l,e)),f=setTimeout(()=>{u=!0,killTree(m),w=setTimeout(async()=>{await killTree(m,"SIGKILL")||d(l,"review check cleanup could not confirm process-tree termination"),h=setTimeout(()=>{killTree(m,"SIGKILL"),d(l,"review check process did not report close after forced termination"),v(null)},1e3)},s)},n),m.once("error",e=>d(l,e?.message||e)),m.once("close",e=>v(e))})}export async function runReviewCheck({target:e,checkId:o,snapshots:n,env:s=process.env,tmpdir:a=r.tmpdir(),timeoutMs:c}={}){const d=validateReviewTarget(e,n),l=await t.mkdtemp(i.join(a,"tp-review-")),m=i.join(l,"source.tar"),u=i.join(l,"tree");try{await t.mkdir(u);const r=await runProcess("git",["-C",d.cwd,"archive","--format=tar",`--output=${m}`,d.sha],{env:await reviewCheckEnv(s,l),timeoutMs:15e3});if(0!==r.exitCode||r.timedOut)return{pass:!1,target:e,sha:d.sha,command:["git","archive"],exitCode:r.exitCode,...r};const i=await runProcess("tar",["-xf",m,"-C",u],{env:await reviewCheckEnv(s,l),timeoutMs:15e3});if(0!==i.exitCode||i.timedOut)return{pass:!1,target:e,sha:d.sha,command:["tar","-xf"],exitCode:i.exitCode,...i};const n=reviewCheckCommand(o),a=await runProcess(n.command,n.args,{cwd:u,env:await reviewCheckEnv(s,l),timeoutMs:c});return{pass:0===a.exitCode&&!a.timedOut,target:e,sha:d.sha,command:[n.command,...n.args],...a}}finally{await t.rm(l,{recursive:!0,force:!0})}}
|
package/runtime-contract.mjs
CHANGED
|
@@ -1,93 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// transport; this module only admits the stable room-facing meaning.
|
|
3
|
-
|
|
4
|
-
const RUNTIMES = Object.freeze(['claude', 'codex', 'hermes'])
|
|
5
|
-
const SUPPORT = Object.freeze({ claude: 'native', codex: 'native', hermes: 'native' })
|
|
6
|
-
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
|
|
7
|
-
const HOST_PATH = /(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i
|
|
8
|
-
const SECRET_KEY = /(secret|token|password|authorization|api.?key|private.?key)/i
|
|
9
|
-
const SECRET_VALUE = /(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i
|
|
10
|
-
const PROHIBITED_PROSE = /(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i
|
|
11
|
-
|
|
12
|
-
const schema = (required = [], properties = {}) => Object.freeze({
|
|
13
|
-
type: 'object', additionalProperties: false, required: Object.freeze(required), properties: Object.freeze(properties),
|
|
14
|
-
})
|
|
15
|
-
const text = (maxLength = 500) => Object.freeze({ type: 'string', minLength: 1, maxLength })
|
|
16
|
-
const optionalText = (maxLength = 500) => Object.freeze({ type: 'string', maxLength })
|
|
17
|
-
const bool = Object.freeze({ type: 'boolean' })
|
|
18
|
-
const integer = (minimum, maximum) => Object.freeze({ type: 'integer', minimum, maximum })
|
|
19
|
-
const id = Object.freeze({ type: 'string', minLength: 1, maxLength: 160, pattern: '^[A-Za-z0-9][A-Za-z0-9._:@-]*$' })
|
|
20
|
-
const option = schema(['label', 'description'], { label: text(80), description: text(240) })
|
|
21
|
-
const question = schema(['id', 'header', 'question', 'options'], {
|
|
22
|
-
id: Object.freeze({ type: 'string', minLength: 1, maxLength: 80, pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$' }),
|
|
23
|
-
header: text(12), question: text(500), options: Object.freeze({ type: 'array', minItems: 2, maxItems: 3, items: option }), multiSelect: bool,
|
|
24
|
-
})
|
|
25
|
-
const evidenceRef = schema(['type', 'id'], { type: id, id })
|
|
26
|
-
|
|
27
|
-
export const RUNTIME_CAPABILITY_VERSION = 1
|
|
28
|
-
export const RUNTIME_CAPABILITIES = Object.freeze([
|
|
29
|
-
Object.freeze({ id: 'request_user_input', version: 1, runtimeSupport: SUPPORT, inputSchema: schema(['questions'], { questions: Object.freeze({ type: 'array', minItems: 1, maxItems: 3, items: question }), autoResolutionMs: integer(60_000, 240_000) }), output: 'accepted | deferred | rejected | failed', authority: 'pair-control', persistence: 'durable-safe-ref', continuation: 'resumable', secretFreeRoomPayload: true }),
|
|
30
|
-
Object.freeze({ id: 'request_permission', version: 1, runtimeSupport: SUPPORT, inputSchema: schema(['action'], { action: text(160), reason: optionalText(280) }), output: 'accepted | deferred | rejected | failed', authority: 'pair-control', persistence: 'durable-safe-ref', continuation: 'resumable', secretFreeRoomPayload: true }),
|
|
31
|
-
Object.freeze({ id: 'submit_flow_plan', version: 1, runtimeSupport: SUPPORT, inputSchema: schema(['plan'], { plan: text(32_768) }), output: 'accepted | rejected | failed', authority: 'pair-control', persistence: 'durable-safe-ref', continuation: 'resumable', secretFreeRoomPayload: true }),
|
|
32
|
-
Object.freeze({ id: 'mark_flow_done', version: 1, runtimeSupport: SUPPORT, inputSchema: schema([], { evidenceRef }), output: 'accepted | rejected | failed', authority: 'bridge', persistence: 'durable-safe-ref', continuation: 'none', secretFreeRoomPayload: true }),
|
|
33
|
-
Object.freeze({ id: 'submit_flow_review', version: 1, runtimeSupport: SUPPORT, inputSchema: schema(['verdict'], { verdict: text(16_384) }), output: 'accepted | rejected | failed', authority: 'bridge', persistence: 'durable-safe-ref', continuation: 'none', secretFreeRoomPayload: true }),
|
|
34
|
-
Object.freeze({ id: 'terminal_interrupt', version: 1, runtimeSupport: SUPPORT, inputSchema: schema([], { reason: optionalText(280) }), output: 'accepted | rejected | failed', authority: 'bridge', persistence: 'ephemeral', continuation: 'none', secretFreeRoomPayload: true }),
|
|
35
|
-
Object.freeze({ id: 'lane_continuation', version: 1, runtimeSupport: SUPPORT, inputSchema: schema(['state'], { state: Object.freeze({ type: 'string', enum: Object.freeze(['waiting_for_human', 'resumable', 'canceled', 'terminal']) }), resumeKind: Object.freeze({ type: 'string', enum: Object.freeze(['human_response', 'approval', 'redispatch', 'reconnect']) }) }), output: 'accepted | deferred | rejected | failed', authority: 'bridge', persistence: 'durable-safe-ref', continuation: 'resumable', secretFreeRoomPayload: true }),
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
const CAPABILITIES = new Map(RUNTIME_CAPABILITIES.map((capability) => [capability.id, capability]))
|
|
39
|
-
export const runtimeCapability = (id) => CAPABILITIES.get(id) || null
|
|
40
|
-
export const runtimeSupportsCapability = (runtime, capabilityId) => RUNTIMES.includes(runtime) && runtimeCapability(capabilityId)?.runtimeSupport?.[runtime] === 'native'
|
|
41
|
-
|
|
42
|
-
const jsonBytes = (value) => {
|
|
43
|
-
try { return new TextEncoder().encode(JSON.stringify(value)).length } catch { return Infinity }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// This deliberately mirrors the pair-control redaction boundary without pulling
|
|
47
|
-
// browser source into the published bridge package.
|
|
48
|
-
export function isSecretFreeRoomPayload (value, maxBytes = 8192) {
|
|
49
|
-
if (value == null || jsonBytes(value) > maxBytes) return false
|
|
50
|
-
const visit = (node) => {
|
|
51
|
-
if (typeof node === 'string') return node.length <= 2048 && !HOST_PATH.test(node) && !SECRET_VALUE.test(node) && !PROHIBITED_PROSE.test(node)
|
|
52
|
-
if (node == null || typeof node === 'number' || typeof node === 'boolean') return true
|
|
53
|
-
if (Array.isArray(node)) return node.length <= 64 && node.every(visit)
|
|
54
|
-
if (typeof node !== 'object') return false
|
|
55
|
-
return Object.entries(node).every(([key, child]) => !SECRET_KEY.test(key) && visit(child))
|
|
56
|
-
}
|
|
57
|
-
return visit(value)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function validValue (value, definition) {
|
|
61
|
-
if (!definition) return true
|
|
62
|
-
if (definition.type === 'object') {
|
|
63
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
64
|
-
if (definition.additionalProperties === false && Object.keys(value).some((key) => !Object.hasOwn(definition.properties || {}, key))) return false
|
|
65
|
-
if ((definition.required || []).some((key) => !Object.hasOwn(value, key))) return false
|
|
66
|
-
return Object.entries(definition.properties || {}).every(([key, child]) => !Object.hasOwn(value, key) || validValue(value[key], child))
|
|
67
|
-
}
|
|
68
|
-
if (definition.type === 'array') return Array.isArray(value) && value.length >= (definition.minItems || 0) && value.length <= (definition.maxItems ?? Infinity) && (!definition.items || value.every((item) => validValue(item, definition.items)))
|
|
69
|
-
if (definition.type === 'string') return typeof value === 'string' && value.length >= (definition.minLength || 0) && value.length <= (definition.maxLength ?? Infinity) && (!definition.enum || definition.enum.includes(value)) && (!definition.pattern || new RegExp(definition.pattern).test(value))
|
|
70
|
-
if (definition.type === 'integer') return Number.isSafeInteger(value) && value >= (definition.minimum ?? -Infinity) && value <= (definition.maximum ?? Infinity)
|
|
71
|
-
if (definition.type === 'boolean') return typeof value === 'boolean'
|
|
72
|
-
return false
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function validateRuntimeCapabilityInput (capabilityId, input) {
|
|
76
|
-
const capability = runtimeCapability(capabilityId)
|
|
77
|
-
return !!capability && validValue(input, capability.inputSchema)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function admitRuntimeCapability ({ runtime, capabilityId, input = {}, roomPayload = null } = {}) {
|
|
81
|
-
const capability = runtimeCapability(capabilityId)
|
|
82
|
-
if (!capability) return Object.freeze({ ok: false, code: 'unregistered_capability' })
|
|
83
|
-
if (!RUNTIMES.includes(runtime) || capability.runtimeSupport[runtime] !== 'native') return Object.freeze({ ok: false, code: 'runtime_capability_mismatch' })
|
|
84
|
-
if (!validateRuntimeCapabilityInput(capabilityId, input)) return Object.freeze({ ok: false, code: 'invalid_capability_input' })
|
|
85
|
-
if (capability.secretFreeRoomPayload && roomPayload != null && !isSecretFreeRoomPayload(roomPayload)) return Object.freeze({ ok: false, code: 'unsafe_room_payload' })
|
|
86
|
-
return Object.freeze({ ok: true, code: 'admitted', capability })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function assertRuntimeCapability (request) {
|
|
90
|
-
const admitted = admitRuntimeCapability(request)
|
|
91
|
-
if (!admitted.ok) throw new TypeError(`Runtime capability rejected: ${admitted.code}`)
|
|
92
|
-
return admitted.capability
|
|
93
|
-
}
|
|
1
|
+
const e=Object.freeze(["claude","codex","hermes"]),t=Object.freeze({claude:"native",codex:"native",hermes:"native"}),r=/(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i,i=/(secret|token|password|authorization|api.?key|private.?key)/i,a=/(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i,n=/(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i,o=(e=[],t={})=>Object.freeze({type:"object",additionalProperties:!1,required:Object.freeze(e),properties:Object.freeze(t)}),c=(e=500)=>Object.freeze({type:"string",minLength:1,maxLength:e}),p=(e=500)=>Object.freeze({type:"string",maxLength:e}),s=Object.freeze({type:"boolean"}),u=Object.freeze({type:"string",minLength:1,maxLength:160,pattern:"^[A-Za-z0-9][A-Za-z0-9._:@-]*$"}),m=o(["label","description"],{label:c(80),description:c(240)}),d=o(["id","header","question","options"],{id:Object.freeze({type:"string",minLength:1,maxLength:80,pattern:"^[A-Za-z0-9][A-Za-z0-9_-]*$"}),header:c(12),question:c(500),options:Object.freeze({type:"array",minItems:2,maxItems:3,items:m}),multiSelect:s}),l=o(["type","id"],{type:u,id:u});export const RUNTIME_CAPABILITY_VERSION=1;export const RUNTIME_CAPABILITIES=Object.freeze([Object.freeze({id:"request_user_input",version:1,runtimeSupport:t,inputSchema:o(["questions"],{questions:Object.freeze({type:"array",minItems:1,maxItems:3,items:d}),autoResolutionMs:Object.freeze({type:"integer",minimum:6e4,maximum:24e4})}),output:"accepted | deferred | rejected | failed",authority:"pair-control",persistence:"durable-safe-ref",continuation:"resumable",secretFreeRoomPayload:!0}),Object.freeze({id:"request_permission",version:1,runtimeSupport:t,inputSchema:o(["action"],{action:c(160),reason:p(280)}),output:"accepted | deferred | rejected | failed",authority:"pair-control",persistence:"durable-safe-ref",continuation:"resumable",secretFreeRoomPayload:!0}),Object.freeze({id:"submit_flow_plan",version:1,runtimeSupport:t,inputSchema:o(["plan"],{plan:c(32768)}),output:"accepted | rejected | failed",authority:"pair-control",persistence:"durable-safe-ref",continuation:"resumable",secretFreeRoomPayload:!0}),Object.freeze({id:"mark_flow_done",version:1,runtimeSupport:t,inputSchema:o([],{evidenceRef:l}),output:"accepted | rejected | failed",authority:"bridge",persistence:"durable-safe-ref",continuation:"none",secretFreeRoomPayload:!0}),Object.freeze({id:"submit_flow_review",version:1,runtimeSupport:t,inputSchema:o(["verdict"],{verdict:c(16384)}),output:"accepted | rejected | failed",authority:"bridge",persistence:"durable-safe-ref",continuation:"none",secretFreeRoomPayload:!0}),Object.freeze({id:"terminal_interrupt",version:1,runtimeSupport:t,inputSchema:o([],{reason:p(280)}),output:"accepted | rejected | failed",authority:"bridge",persistence:"ephemeral",continuation:"none",secretFreeRoomPayload:!0}),Object.freeze({id:"lane_continuation",version:1,runtimeSupport:t,inputSchema:o(["state"],{state:Object.freeze({type:"string",enum:Object.freeze(["waiting_for_human","resumable","canceled","terminal"])}),resumeKind:Object.freeze({type:"string",enum:Object.freeze(["human_response","approval","redispatch","reconnect"])})}),output:"accepted | deferred | rejected | failed",authority:"bridge",persistence:"durable-safe-ref",continuation:"resumable",secretFreeRoomPayload:!0})]);const b=new Map(RUNTIME_CAPABILITIES.map(e=>[e.id,e]));export const runtimeCapability=e=>b.get(e)||null;export const runtimeSupportsCapability=(t,r)=>e.includes(t)&&"native"===runtimeCapability(r)?.runtimeSupport?.[t];export function isSecretFreeRoomPayload(e,t=8192){if(null==e||(e=>{try{return(new TextEncoder).encode(JSON.stringify(e)).length}catch{return 1/0}})(e)>t)return!1;const o=e=>"string"==typeof e?e.length<=2048&&!r.test(e)&&!a.test(e)&&!n.test(e):null==e||"number"==typeof e||"boolean"==typeof e||(Array.isArray(e)?e.length<=64&&e.every(o):"object"==typeof e&&Object.entries(e).every(([e,t])=>!i.test(e)&&o(t)));return o(e)}function y(e,t){return!t||("object"===t.type?!(!e||"object"!=typeof e||Array.isArray(e))&&(!1!==t.additionalProperties||!Object.keys(e).some(e=>!Object.hasOwn(t.properties||{},e)))&&!(t.required||[]).some(t=>!Object.hasOwn(e,t))&&Object.entries(t.properties||{}).every(([t,r])=>!Object.hasOwn(e,t)||y(e[t],r)):"array"===t.type?Array.isArray(e)&&e.length>=(t.minItems||0)&&e.length<=(t.maxItems??1/0)&&(!t.items||e.every(e=>y(e,t.items))):"string"===t.type?"string"==typeof e&&e.length>=(t.minLength||0)&&e.length<=(t.maxLength??1/0)&&(!t.enum||t.enum.includes(e))&&(!t.pattern||new RegExp(t.pattern).test(e)):"integer"===t.type?Number.isSafeInteger(e)&&e>=(t.minimum??-1/0)&&e<=(t.maximum??1/0):"boolean"===t.type&&"boolean"==typeof e)}export function validateRuntimeCapabilityInput(e,t){const r=runtimeCapability(e);return!!r&&y(t,r.inputSchema)}export function admitRuntimeCapability({runtime:t,capabilityId:r,input:i={},roomPayload:a=null}={}){const n=runtimeCapability(r);return n?e.includes(t)&&"native"===n.runtimeSupport[t]?validateRuntimeCapabilityInput(r,i)?n.secretFreeRoomPayload&&null!=a&&!isSecretFreeRoomPayload(a)?Object.freeze({ok:!1,code:"unsafe_room_payload"}):Object.freeze({ok:!0,code:"admitted",capability:n}):Object.freeze({ok:!1,code:"invalid_capability_input"}):Object.freeze({ok:!1,code:"runtime_capability_mismatch"}):Object.freeze({ok:!1,code:"unregistered_capability"})}export function assertRuntimeCapability(e){const t=admitRuntimeCapability(e);if(!t.ok)throw new TypeError(`Runtime capability rejected: ${t.code}`);return t.capability}
|
package/runtime-registry.mjs
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { runtimeCapability, runtimeSupportsCapability } from './runtime-contract.mjs'
|
|
3
|
-
|
|
4
|
-
const RUNTIMES = Object.freeze({
|
|
5
|
-
claude: Object.freeze({
|
|
6
|
-
id: 'claude', command: 'claude', label: 'Claude Code', protocol: 'agent-sdk',
|
|
7
|
-
structured: true, flow: true, canSteer: false, images: true, nativeModelCatalog: false, effortControl: true, defaultMode: 'default',
|
|
8
|
-
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']),
|
|
9
|
-
}),
|
|
10
|
-
codex: Object.freeze({
|
|
11
|
-
id: 'codex', command: 'codex', label: 'Codex', protocol: 'codex-app-server',
|
|
12
|
-
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, effortControl: true, defaultMode: 'bypassPermissions',
|
|
13
|
-
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions', 'review']),
|
|
14
|
-
}),
|
|
15
|
-
hermes: Object.freeze({
|
|
16
|
-
id: 'hermes', command: 'thinkpool', label: 'Hermes Agent', protocol: 'acp',
|
|
17
|
-
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, catalogRequiresSession: true, effortControl: true, defaultMode: 'default',
|
|
18
|
-
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']),
|
|
19
|
-
beta: true,
|
|
20
|
-
}),
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
export const STRUCTURED_RUNTIME_IDS = Object.freeze(Object.keys(RUNTIMES))
|
|
24
|
-
export const structuredRuntimeMetadata = (runtime) => RUNTIMES[runtime] || null
|
|
25
|
-
export const structuredRuntimeForCommand = (command) => {
|
|
26
|
-
const bin = path.basename(String(command || '')).toLowerCase()
|
|
27
|
-
return Object.values(RUNTIMES).find((runtime) => runtime.command === bin)?.id || null
|
|
28
|
-
}
|
|
29
|
-
export const defaultStructuredMode = (runtime) => structuredRuntimeMetadata(runtime)?.defaultMode || 'default'
|
|
30
|
-
export const structuredRuntimeSupportsMode = (runtime, mode) => structuredRuntimeMetadata(runtime)?.modes?.includes(mode) === true
|
|
31
|
-
export const structuredRuntimeSupportsFlow = (runtime) => structuredRuntimeMetadata(runtime)?.flow === true
|
|
32
|
-
// This is deliberately semantic rather than a list of native tool names. The
|
|
33
|
-
// bridge may expose an MCP tool, an SDK hook, or ACP registration underneath.
|
|
34
|
-
export const structuredRuntimeCapability = (runtime, capabilityId) => (
|
|
35
|
-
runtimeSupportsCapability(runtime, capabilityId) ? runtimeCapability(capabilityId) : null
|
|
36
|
-
)
|
|
37
|
-
export const structuredModeLocked = ({ flowRole, sliceType } = {}) => (
|
|
38
|
-
flowRole === 'conductor' || flowRole === 'reviewer' || sliceType === 'review'
|
|
39
|
-
)
|
|
40
|
-
export const structuredModeForSlice = (runtime, lane = {}) => {
|
|
41
|
-
if (lane.flowRole !== 'reviewer' && lane.sliceType !== 'review') return lane.mode
|
|
42
|
-
if (structuredRuntimeSupportsMode(runtime, 'review')) return 'review'
|
|
43
|
-
if (structuredRuntimeSupportsMode(runtime, 'plan')) return 'plan'
|
|
44
|
-
return lane.mode
|
|
45
|
-
}
|
|
46
|
-
export const structuredModesForLane = (runtime, lane = {}) => {
|
|
47
|
-
const modes = structuredRuntimeMetadata(runtime)?.modes || []
|
|
48
|
-
return structuredModeLocked(lane) && modes.includes(lane.mode) ? [lane.mode] : modes
|
|
49
|
-
}
|
|
50
|
-
const STRUCTURED_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
|
|
51
|
-
export const normalizeStructuredEffort = (runtime, effort) => {
|
|
52
|
-
if (effort === null) return null
|
|
53
|
-
if (runtime === 'hermes' && effort === 'none') return 'none'
|
|
54
|
-
return STRUCTURED_EFFORTS.has(effort) ? effort : 'high'
|
|
55
|
-
}
|
|
56
|
-
export const shouldDeferStructuredRuntime = ({ runtime, defer, cwd, flowSessionId, flowTaskKey, models } = {}) => {
|
|
57
|
-
const metadata = structuredRuntimeMetadata(runtime)
|
|
58
|
-
// Some ACP agents publish their model catalog only from session/new or
|
|
59
|
-
// session/resume. A brand-new lane must perform that zero-inference handshake
|
|
60
|
-
// before its first prompt or the model picker has no truthful choices to show.
|
|
61
|
-
// Once the exact catalog is persisted, the normal lazy-start policy resumes.
|
|
62
|
-
if (metadata?.catalogRequiresSession && !(Array.isArray(models) && models.length)) return false
|
|
63
|
-
return !!defer || (!cwd && !flowSessionId && !flowTaskKey)
|
|
64
|
-
}
|
|
1
|
+
import e from"node:path";import{runtimeCapability as t,runtimeSupportsCapability as o}from"./runtime-contract.mjs";const r=Object.freeze({claude:Object.freeze({id:"claude",command:"claude",label:"Claude Code",protocol:"agent-sdk",structured:!0,flow:!0,canSteer:!1,images:!0,nativeModelCatalog:!1,effortControl:!0,defaultMode:"default",modes:Object.freeze(["default","acceptEdits","plan","bypassPermissions"])}),codex:Object.freeze({id:"codex",command:"codex",label:"Codex",protocol:"codex-app-server",structured:!0,flow:!0,canSteer:!0,images:!0,nativeModelCatalog:!0,effortControl:!0,defaultMode:"bypassPermissions",modes:Object.freeze(["default","acceptEdits","plan","bypassPermissions","review"])}),hermes:Object.freeze({id:"hermes",command:"thinkpool",label:"Hermes Agent",protocol:"acp",structured:!0,flow:!0,canSteer:!0,images:!0,nativeModelCatalog:!0,catalogRequiresSession:!0,effortControl:!0,defaultMode:"default",modes:Object.freeze(["default","acceptEdits","plan","bypassPermissions"]),beta:!0})});export const STRUCTURED_RUNTIME_IDS=Object.freeze(Object.keys(r));export const structuredRuntimeMetadata=e=>r[e]||null;export const structuredRuntimeForCommand=t=>{const o=e.basename(String(t||"")).toLowerCase();return Object.values(r).find(e=>e.command===o)?.id||null};export const defaultStructuredMode=e=>structuredRuntimeMetadata(e)?.defaultMode||"default";export const structuredRuntimeSupportsMode=(e,t)=>!0===structuredRuntimeMetadata(e)?.modes?.includes(t);export const structuredRuntimeSupportsFlow=e=>!0===structuredRuntimeMetadata(e)?.flow;export const structuredRuntimeCapability=(e,r)=>o(e,r)?t(r):null;export const structuredModeLocked=({flowRole:e,sliceType:t}={})=>"conductor"===e||"reviewer"===e||"review"===t;export const structuredModeForSlice=(e,t={})=>"reviewer"!==t.flowRole&&"review"!==t.sliceType?t.mode:structuredRuntimeSupportsMode(e,"review")?"review":structuredRuntimeSupportsMode(e,"plan")?"plan":t.mode;export const structuredModesForLane=(e,t={})=>{const o=structuredRuntimeMetadata(e)?.modes||[];return structuredModeLocked(t)&&o.includes(t.mode)?[t.mode]:o};const s=new Set(["low","medium","high","xhigh","max"]);export const normalizeStructuredEffort=(e,t)=>null===t?null:"hermes"===e&&"none"===t?"none":s.has(t)?t:"high";export const shouldDeferStructuredRuntime=({runtime:e,defer:t,cwd:o,flowSessionId:r,flowTaskKey:s,models:d}={})=>{const u=structuredRuntimeMetadata(e);return!(!(!u?.catalogRequiresSession||Array.isArray(d)&&d.length)||!t&&(o||r||s))};
|
package/runtime-session.mjs
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { startCodexSession } from './codex-session.mjs'
|
|
3
|
-
import { startHermesSession } from './hermes-session.mjs'
|
|
4
|
-
import { assertRuntimeCapability } from './runtime-contract.mjs'
|
|
5
|
-
|
|
6
|
-
const FACTORIES = Object.freeze({
|
|
7
|
-
claude: startClaudeSession,
|
|
8
|
-
codex: startCodexSession,
|
|
9
|
-
hermes: startHermesSession,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
export function startStructuredSession(runtime, options) {
|
|
13
|
-
const factory = FACTORIES[runtime]
|
|
14
|
-
if (!factory) throw new Error(`Unsupported structured runtime: ${runtime}`)
|
|
15
|
-
// Every live adapter has a native interrupt boundary. Assert the semantic
|
|
16
|
-
// contract here so an accidental registry/runtime divergence fails at launch,
|
|
17
|
-
// rather than silently accepting a provider-specific fallback later.
|
|
18
|
-
assertRuntimeCapability({ runtime, capabilityId: 'terminal_interrupt', input: {} })
|
|
19
|
-
return factory(options)
|
|
20
|
-
}
|
|
1
|
+
import{startClaudeSession as r}from"./claude-session.mjs";import{startCodexSession as t}from"./codex-session.mjs";import{startHermesSession as e}from"./hermes-session.mjs";import{assertRuntimeCapability as o}from"./runtime-contract.mjs";const s=Object.freeze({claude:r,codex:t,hermes:e});export function startStructuredSession(r,t){const e=s[r];if(!e)throw new Error(`Unsupported structured runtime: ${r}`);return o({runtime:r,capabilityId:"terminal_interrupt",input:{}}),e(t)}
|