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.
Files changed (117) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6240
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -0
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -91
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -0
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +15 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-capabilities.json +11 -2
  111. package/thinkpool-prompt-contracts.mjs +1 -85
  112. package/thinkpool-room-prompt.mjs +1 -187
  113. package/transcript-sanitize.mjs +1 -332
  114. package/turn-stall.mjs +1 -61
  115. package/update-gate.mjs +1 -53
  116. package/viewport.mjs +1 -810
  117. package/worker-completion.mjs +1 -57
package/repo-search.mjs CHANGED
@@ -1,190 +1 @@
1
- import { execFileSync } from 'node:child_process'
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
- // Fixed, bridge-owned review checks. Models receive only { target, checkId }.
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})}}
@@ -1,93 +1 @@
1
- // Bridge-owned semantic capabilities. Native runtimes keep their own tool
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}
@@ -1,64 +1 @@
1
- import path from 'node:path'
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))};
@@ -1,20 +1 @@
1
- import { startClaudeSession } from './claude-session.mjs'
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)}