synabun 2.2.0 → 2.2.1

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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -1
  3. package/mcp-server/package-lock.json +2 -2
  4. package/mcp-server/package.json +1 -1
  5. package/neural-interface/i18n/en.json +3 -0
  6. package/neural-interface/i18n/pt-BR.json +3 -0
  7. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  8. package/neural-interface/lib/assistant-dispatch.js +4 -1
  9. package/neural-interface/lib/mcp-external-servers.js +27 -3
  10. package/neural-interface/lib/native-loop-providers.js +3 -2
  11. package/neural-interface/lib/opencode-native-boundary.js +155 -21
  12. package/neural-interface/lib/peer-identity.js +49 -5
  13. package/neural-interface/package-lock.json +2 -2
  14. package/neural-interface/package.json +1 -1
  15. package/neural-interface/public/shared/assistant/asst-api.js +14 -2
  16. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  17. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  18. package/neural-interface/public/shared/assistant/asst-panel.js +1 -1
  19. package/neural-interface/public/shared/assistant/asst-render.js +62 -7
  20. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  21. package/neural-interface/public/shared/assistant/asst-scenes.js +225 -79
  22. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  23. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  24. package/neural-interface/public/shared/assistant/asst-styles.js +24 -4
  25. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  26. package/neural-interface/public/shared/loading-teardown.js +34 -0
  27. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  28. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +1 -0
  29. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +10 -0
  30. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  31. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +88 -72
  32. package/neural-interface/public/shared/storage-retry.js +47 -0
  33. package/neural-interface/public/shared/storage.js +25 -4
  34. package/neural-interface/public/shared/styles.css +8 -1
  35. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  36. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  37. package/neural-interface/public/shared/ui-claude-panel.js +79 -61
  38. package/neural-interface/public/shared/ui-loading.js +5 -4
  39. package/neural-interface/server.js +87 -40
  40. package/npm-shrinkwrap.json +2 -2
  41. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { readStoredToolListRecord, toolListRecordForUpdate } from './lib/tool-lists.js';
2
- import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues, mcpDefinitionValues } from './lib/mcp-external-servers.js';
2
+ import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues, mcpSecretValues } from './lib/mcp-external-servers.js';
3
3
  import { openCodeWorkerData } from './lib/opencode-worker-data.js';
4
4
  import { resumeSandboxPlan } from './lib/worker-resume.js';
5
5
  import { waitForSynabunTools, synabunConfigured, invalidateSynabunTools, synabunToolsTimeoutText } from './lib/opencode-mcp-ready.js';
@@ -25,7 +25,7 @@ import { SANDBOX_EXEC, buildWorkerSandboxProfile, canonicalPath, sandboxExecArgv
25
25
  import { createWorkerMcpChannel } from './lib/worker-mcp-channel.js';
26
26
  import { createPageSession, PAGE_SECRET_PATHS, PAGE_SECRET_REFRESH_PATH, SERVICE_WORKER_PATH } from './lib/page-session.js';
27
27
  import { normalizeViewUrl } from './public/shared/view-variant.js';
28
- import { createPeerIdentity } from './lib/peer-identity.js';
28
+ import { createPeerIdentity, debugClientPolicy } from './lib/peer-identity.js';
29
29
  import { createOwnerAuthority } from './lib/owner-authority.js';
30
30
  import { collectRelayListenerPorts, localProxyPorts } from './lib/worker-relay-ports.js';
31
31
  import { machineNames } from './lib/machine-names.js';
@@ -205,7 +205,7 @@ import {
205
205
  } from './lib/native-loop-providers.js';
206
206
  import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
207
207
  import { browserServePortDenial } from './lib/browser-tool-policy.js';
208
- import { createNativeServeBoundary, nativeServeCapability } from './lib/opencode-native-boundary.js';
208
+ import { createNativeServeBoundary, nativeServeCapability, nativeServeReason, narrowOffenders } from './lib/opencode-native-boundary.js';
209
209
  const nativeServeBoundary = createNativeServeBoundary();
210
210
  import { setBrowserNetworkRule, usesNativeBrowserNetworkGuard, disposeBrowserNetworkGuard, recoverBrowserNetworkGuard, MONEY_PATTERNS, SERVE_PORT_PATTERNS } from './lib/browser-network-guard.js';
211
211
  import { browserPolicyRoots } from './lib/browser-policy-roots.js';
@@ -447,6 +447,9 @@ try {
447
447
  // allowed until a restart. What the environment itself gives is noted first and put back after the load; the
448
448
  // file's own line is read when a request asks (publicWebOrigins).
449
449
  const startedAllowedHosts = startedValueOf(process.env);
450
+ // The profiling exemption (lib/peer-identity.js debugClientPolicy) is read from the started environment only, before
451
+ // the data home's .env can add to it.
452
+ const startedDebugClient = debugClientPolicy(process.env);
450
453
  dotenvConfig({ path: resolve(getDataHome(), '.env') });
451
454
  startedAllowedHosts.restore();
452
455
 
@@ -556,7 +559,7 @@ app.use(requestGates.features);
556
559
  // in front of every route (the page secret's pieces are made further down and read only when a request comes).
557
560
  // This install's root (PACKAGE_ROOT, defined further down): SynaBun's own MCP server over HTTP is the one listening
558
561
  // program the lookup knows, by its main script under it (mcp-server/dist/http.js, `npm run start:http`).
559
- const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')] });
562
+ const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')], debugClient: startedDebugClient });
560
563
  // The one decision for the owner's routes and sockets (lib/owner-authority.js): every write to /api but the hooks'
561
564
  // and the agents' feature data, the two reads that hand out a secret, and the owner's sockets are refused to the
562
565
  // automation browser (its marker or its process), relays, other SynaBun servers, remote logins and an agent's tree
@@ -3755,22 +3758,25 @@ function opencodeServeLaunch(port, { env = process.env, password = null, passwor
3755
3758
  _noteCliLookup('opencode', found, 'OpenCode');
3756
3759
  const line = configuredCommandLine('opencode');
3757
3760
  const nativeWebSocketSafe = nativeServeCapability({ version: found.version, commandLine: !!line, passwordFrom });
3758
- nativeServeBoundary.assertLaunch(nativeWebSocketSafe);
3761
+ // What the browser boundary names when this serve is the one that blocks a native (MoreLogin) context. A command
3762
+ // line from Settings runs whatever it names: the resolver's version is of another binary, so none is reported.
3763
+ const nativeServe = { version: line ? null : (found.version || null), path: line || found.launchPath || found.path || null, reason: nativeServeReason({ version: found.version, commandLine: !!line, passwordFrom }) };
3764
+ nativeServeBoundary.assertLaunch(nativeWebSocketSafe, { port, ...nativeServe });
3759
3765
  const spec = serveSpecFor(port, { env, password, passwordFrom });
3760
3766
  const words = spec.args.join(' ');
3761
3767
  if (IS_WIN) {
3762
3768
  const spawnEnv = withPathEnv(spec.env, getAugmentedPath());
3763
3769
  if (!line && found.spec) {
3764
3770
  const launch = spawnArgsFor(found.spec, spec.args, { env: spawnEnv });
3765
- return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3771
+ return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3766
3772
  }
3767
3773
  const text = `${line || 'opencode'} ${words}`;
3768
- return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3774
+ return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3769
3775
  }
3770
3776
  const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
3771
3777
  const unset = spec.passwordFrom === 'stdout' ? `${serveUnsetCommand()}; ` : '';
3772
3778
  // A packaged application's runtime and npm after the user's own (the login shell adds the user's profile on top).
3773
- return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3779
+ return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3774
3780
  }
3775
3781
 
3776
3782
  // Which OpenCode the binary SynaBun starts is: the resolver's `--version` of it
@@ -8261,8 +8267,8 @@ function noteOpencodeServePort(port, delta) {
8261
8267
  if (next > 0) _opencodeServePortCounts.set(p, next); else _opencodeServePortCounts.delete(p);
8262
8268
  }
8263
8269
 
8264
- function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false }) {
8265
- nativeServeBoundary.track(proc, { port, safe: nativeWebSocketSafe });
8270
+ function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false, nativeServe = null }) {
8271
+ nativeServeBoundary.track(proc, { ...(nativeServe || {}), port, safe: nativeWebSocketSafe });
8266
8272
  const pid = Number(proc?.pid) || null;
8267
8273
  const exited = () => !proc || proc.exitCode != null || proc.signalCode != null;
8268
8274
  if (!exited()) {
@@ -8788,7 +8794,7 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, c
8788
8794
  entry.proc = proc;
8789
8795
  entry.password = launch.password || null;
8790
8796
  // On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
8791
- const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
8797
+ const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
8792
8798
  // While it boots, its start sees an exit itself (another server on the port is a collision); afterwards
8793
8799
  // its exit ends the serve.
8794
8800
  let booting = true;
@@ -9658,7 +9664,7 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
9658
9664
  });
9659
9665
  _ocpProc = proc;
9660
9666
  // On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
9661
- tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
9667
+ tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
9662
9668
  const readPassword = servePasswordReader();
9663
9669
  const outLog = createServeLogRedactor(line => console.log(`[opencode:stdout] ${line.trim()}`));
9664
9670
  const errLog = createServeLogRedactor(line => console.log(`[opencode:stderr] ${line.trim()}`));
@@ -14609,7 +14615,7 @@ app.post('/api/loop/launch', async (req, res) => {
14609
14615
  const acq = await acquireLoopBrowserAndTab({ requestedBrowserSessionId, terminalSessionId });
14610
14616
  if (acq.error) {
14611
14617
  loopLog(terminalSessionId, 'launch:browser', 'ABORT: browser acquisition failed — returning 500', { error: acq.error });
14612
- return res.status(500).json({ error: acq.error });
14618
+ return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
14613
14619
  }
14614
14620
  browserSessionId = acq.browserSessionId;
14615
14621
  browserTabId = acq.browserTabId;
@@ -14935,7 +14941,7 @@ app.post('/api/loop/recover-browser', async (req, res) => {
14935
14941
  const terminalSessionId = req.body?.terminalSessionId || randomBytes(16).toString('hex');
14936
14942
  const acq = await acquireLoopBrowserAndTab({ terminalSessionId, logTag: 'recover:browser' });
14937
14943
  if (acq.error) {
14938
- return res.status(500).json({ error: acq.error });
14944
+ return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
14939
14945
  }
14940
14946
  // Persist the new pins onto the loop's state file when we can identify it,
14941
14947
  // so resolve-from-ancestors and loop/stop operate on the live session/tab.
@@ -16934,7 +16940,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
16934
16940
  const sessionId = agent._activeSessionId || agent.sessionId;
16935
16941
 
16936
16942
  const serverMap = JSON.parse(agent._mcpConfig).mcpServers;
16937
- const mcpValues = Object.values(serverMap).flatMap(entry => [...Object.values(entry.env || {}), ...Object.values(entry.headers || {}), ...(entry.args || []), ...(entry.url ? [entry.url] : [])]).filter(value => typeof value === 'string' && value);
16943
+ const mcpValues = mcpSecretValues(serverMap, { env: process.env });
16938
16944
  const mcpConfigPath = writePrivateMcpConfig(resolve(DATA_HOME, 'data', 'agents', agent.id, 'mcp.json'), serverMap);
16939
16945
  const args = [
16940
16946
  '--print',
@@ -17415,7 +17421,7 @@ app.post('/api/agents/launch', async (req, res) => {
17415
17421
  const acq = await acquireSharedBrowserTab({ ownerKey: `agent:${agentId}`, ownerType: 'interactive', logTag: 'agent:browser' });
17416
17422
  if (acq.error) {
17417
17423
  console.warn(`[agent] Browser tab acquire failed: ${acq.error}`);
17418
- return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}` });
17424
+ return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}`, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
17419
17425
  }
17420
17426
  browserSessionId = acq.browserSessionId;
17421
17427
  browserTabId = acq.browserTabId;
@@ -18668,7 +18674,7 @@ const editorCompletionServe = createOpencodeCompletionEngine({
18668
18674
  const token = opencodeServeRecords.newToken();
18669
18675
  const launch = opencodeServeLaunch(port, { env: { ...env, [SERVE_TOKEN_ENV]: token }, passwordFrom: 'stdout' });
18670
18676
  const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'pipe', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
18671
- editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe }));
18677
+ editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe }));
18672
18678
  const outLog = createServeLogRedactor(line => console.log(`[editor-complete:out] ${line.trim().slice(0, 500)}`));
18673
18679
  const errLog = createServeLogRedactor(line => console.log(`[editor-complete:err] ${line.trim().slice(0, 500)}`));
18674
18680
  proc.stdout?.on('data', d => outLog.push(d));
@@ -25209,7 +25215,7 @@ function externalToolsCatalog(cwd = null) {
25209
25215
  function applyOpenCodeExternalScope(xdgRoot, config, tools, cwd, { readProject = true } = {}) {
25210
25216
  const scoped = scopeOpenCodeServers(config, tools, externalInventory('opencode', cwd || PACKAGE_ROOT, { readProject }));
25211
25217
  Object.assign(config, scoped.config);
25212
- _externalMcpLogValues.set(xdgRoot, mcpDefinitionValues(scoped.config.mcp.servers));
25218
+ _externalMcpLogValues.set(xdgRoot, mcpSecretValues(scoped.config.mcp.servers));
25213
25219
  writeFileSync(resolve(xdgRoot, 'synabun-external-scope.json'), JSON.stringify(scoped.scope), { mode: 0o600 });
25214
25220
  return scoped.scope;
25215
25221
  }
@@ -30991,7 +30997,7 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
30991
30997
  profileMode: result.profileMode, profileSource: result.profileSource,
30992
30998
  });
30993
30999
  } catch (err) {
30994
- createError = err.message;
31000
+ createError = browserStartFailure(err);
30995
31001
  loopLog(ownerKey, logTag, 'strategy 2 FAILED: createBrowserSession threw', { err: err.message, stack: err.stack?.slice(0, 600) });
30996
31002
  }
30997
31003
  }
@@ -31001,8 +31007,9 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
31001
31007
  // run's log, lastRunResult and the schedule:failed toast.
31002
31008
  return {
31003
31009
  error: createError
31004
- ? `Could not start the shared browser session: ${createError}`
31010
+ ? `Could not start the shared browser session: ${createError.error}`
31005
31011
  : 'Could not start the shared browser session. Open Browser Settings and verify the selected profile can launch.',
31012
+ ...(createError?.code ? { code: createError.code, offenders: createError.offenders } : {}),
31006
31013
  };
31007
31014
  }
31008
31015
 
@@ -32167,34 +32174,72 @@ async function installMoneyGuardRoute(ctx) {
32167
32174
  // still aborted. Managed contexts also block WS through Playwright. MoreLogin
32168
32175
  // retains native WS only behind the verified server authentication boundary and
32169
32176
  // credentials confined to this server, never an environment, page or log.
32170
- function protectNativeServeContext(ctx) {
32171
- return nativeServeBoundary.protect(ctx, {
32172
- ports: liveOpencodeServePorts(),
32173
- unverifiedRecords: opencodeServeRecords.list().filter(record =>
32174
- !opencodeServeRecords.ownedHere(record) && opencodeServeRecords.verifySync(record) !== 'gone'),
32175
- });
32177
+ // Records of serves this server did not start that may still run: each one could answer a page's WebSocket.
32178
+ function unverifiedServeRecords() {
32179
+ return opencodeServeRecords.list().filter(record =>
32180
+ !opencodeServeRecords.ownedHere(record) && opencodeServeRecords.verifySync(record) !== 'gone')
32181
+ .map(record => ({ ...record, claimable: (() => { try { return opencodeServeRecords.claimable(record); } catch { return false; } })() }));
32182
+ }
32183
+ const NATIVE_SERVE_WAIT_MS = Number(process.env.SYNABUN_NATIVE_SERVE_WAIT_MS) || 20_000;
32184
+ const NATIVE_SERVE_SWEEP_MS = Number(process.env.SYNABUN_NATIVE_SERVE_SWEEP_MS) || 10_000;
32185
+ // Left of a request's deadline for the answer itself (the MCP client gives a session create 70 s).
32186
+ const NATIVE_SERVE_REPLY_MARGIN_MS = 5_000;
32187
+ // Before refusing, repair what SynaBun can itself, bounded: stop the serves an ended SynaBun left running (the records'
32188
+ // own sweep, which signals only a provably-SynaBun process whose owner has ended; at most NATIVE_SERVE_SWEEP_MS, in the
32189
+ // background while the rest is read) and wait for SynaBun's own verified serves still starting (at most
32190
+ // NATIVE_SERVE_WAIT_MS), both inside the request's deadline. The context is held by the boundary from before the
32191
+ // first look (nativeServeBoundary.attach), so no unverified serve starts meanwhile. Nothing is cached: every attempt
32192
+ // reads the live state again. Authentication is never relaxed: what is left is named.
32193
+ async function protectNativeServeContext(ctx, { deadline = Infinity, signal = null } = {}) {
32194
+ try {
32195
+ return await nativeServeBoundary.attach(ctx, {
32196
+ state: () => ({ ports: [...liveOpencodeServePorts()], unverifiedRecords: unverifiedServeRecords() }),
32197
+ sweep: () => opencodeServeRecords.sweep(),
32198
+ waitMs: NATIVE_SERVE_WAIT_MS, sweepMs: NATIVE_SERVE_SWEEP_MS,
32199
+ deadline: Number.isFinite(deadline) ? deadline - NATIVE_SERVE_REPLY_MARGIN_MS : Infinity, signal,
32200
+ });
32201
+ } catch (error) {
32202
+ if (error?.offenders) console.warn(`[browser] native serve boundary refused: ${JSON.stringify(error.offenders)}`);
32203
+ throw error;
32204
+ }
32205
+ }
32206
+
32207
+ // The answer a failed browser start gives its caller: the sentence, and for the native serve boundary its code and
32208
+ // the serves it names (narrowed: never a token or a whole record).
32209
+ function browserStartFailure(err) {
32210
+ const out = { error: err?.message || String(err) };
32211
+ if (err?.code === 'NATIVE_SERVE_UNVERIFIED') { out.code = err.code; out.offenders = narrowOffenders(err.offenders); }
32212
+ return out;
32176
32213
  }
32177
32214
 
32178
- async function installServePortGuard(ctx) {
32215
+ async function installServePortGuard(ctx, { deadline = Infinity, signal = null } = {}) {
32179
32216
  if (!ctx || ctx.__servePortGuardInstalled) return;
32180
32217
  // routeWebSocket installs page bindings and replaces WebSocket on navigation,
32181
32218
  // even with an empty matcher. Native contexts rely on the verified server's
32182
32219
  // authentication; never silently route or drop protection for unknown serves.
32183
32220
  const native = usesNativeBrowserNetworkGuard(ctx);
32184
- const release = native ? protectNativeServeContext(ctx) : null;
32185
32221
  const refused = (url) => { try { return !!browserServePortDenial(url?.href ?? url, liveOpencodeServePorts()); } catch { return false; } };
32222
+ const installRule = () => setBrowserNetworkRule(ctx, 'serve-port', {
32223
+ patterns: SERVE_PORT_PATTERNS,
32224
+ decide: request => refused(request.url) ? { abort: true } : null,
32225
+ });
32226
+ if (native) {
32227
+ // Held first (assertLaunch refuses an unverified start from here on), then the HTTP rule, then the bounded check.
32228
+ const release = nativeServeBoundary.reserve(ctx);
32229
+ try { await installRule(); } catch (error) {
32230
+ release();
32231
+ throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
32232
+ }
32233
+ await protectNativeServeContext(ctx, { deadline, signal });
32234
+ ctx.__servePortGuardInstalled = true;
32235
+ return;
32236
+ }
32186
32237
  try {
32187
- await setBrowserNetworkRule(ctx, 'serve-port', {
32188
- patterns: SERVE_PORT_PATTERNS,
32189
- decide: request => refused(request.url) ? { abort: true } : null,
32190
- });
32191
- if (!native) {
32192
- if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
32193
- await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
32194
- }
32238
+ await installRule();
32239
+ if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
32240
+ await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
32195
32241
  ctx.__servePortGuardInstalled = true;
32196
32242
  } catch (error) {
32197
- release?.();
32198
32243
  throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
32199
32244
  }
32200
32245
  }
@@ -32874,7 +32919,7 @@ async function createBrowserSession(options = {}) {
32874
32919
  const startServeDenial = browserServePortDenial(startUrl, liveOpencodeServePorts());
32875
32920
  if (startServeDenial) throw new Error(startServeDenial);
32876
32921
  await installMoneyGuardRoute(context);
32877
- await installServePortGuard(context);
32922
+ await installServePortGuard(context, { deadline: options.browserRequest?.deadline, signal: options.browserRequest?.signal });
32878
32923
  if (typeof _pageSession !== 'undefined') {
32879
32924
  const marked = await _pageSession.markWorkerBrowserContext(context, { port: PORT, hostNames: machineNames.names() });
32880
32925
  if (!marked) throw new Error('Cannot protect browser owner authority: local worker marker failed');
@@ -33989,6 +34034,8 @@ app.post('/api/browser/sessions', async (req, res) => {
33989
34034
  };
33990
34035
  const result = await createBrowserSession({
33991
34036
  url, width, height, screenWidth, screenHeight, deviceScaleFactor, timezone, _realHeaders,
34037
+ // Its deadline bounds the native serve check (installServePortGuard) inside the caller's timeout.
34038
+ browserRequest: req.browserRequest,
33992
34039
  // Optional per-session override to target a specific MoreLogin env (multi-identity)
33993
34040
  ...(connectMode === 'morelogin' ? { connectMode } : {}),
33994
34041
  ...(moreloginEnvId ? { moreloginEnvId: String(moreloginEnvId) } : {}),
@@ -34015,7 +34062,7 @@ app.post('/api/browser/sessions', async (req, res) => {
34015
34062
  });
34016
34063
  } catch (err) {
34017
34064
  console.error('Browser session create error:', err.message);
34018
- res.status(500).json({ error: err.message });
34065
+ res.status(500).json(browserStartFailure(err));
34019
34066
  }
34020
34067
  });
34021
34068
 
@@ -34040,7 +34087,7 @@ app.post('/api/browser/acquire', async (req, res) => {
34040
34087
  const clientId = req.body?.clientId;
34041
34088
  if (!clientId) return res.status(400).json({ error: 'clientId is required' });
34042
34089
  const acq = await acquireSharedBrowserTab({ ownerKey: clientId, ownerType: 'interactive', logTag: 'acquire:interactive' });
34043
- if (acq.error) return res.status(500).json({ error: acq.error });
34090
+ if (acq.error) return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
34044
34091
  res.json({ ok: true, sessionId: acq.browserSessionId, tabId: acq.browserTabId || null });
34045
34092
  } catch (err) {
34046
34093
  res.status(500).json({ error: err.message });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "synabun",
9
- "version": "2.2.0",
9
+ "version": "2.2.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "Apache-2.0",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "mcpName": "io.github.danilokhury/synabun",
5
5
  "description": "Local-first persistent vector memory, MCP tooling, and multi-agent workflows for AI assistants",
6
6
  "license": "Apache-2.0",