thinkpool-pair 0.7.157 → 0.7.159
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/account.mjs +19 -12
- package/bridge.mjs +88 -1
- package/package.json +2 -1
- package/serve-consent.mjs +107 -0
package/account.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import os from 'node:os'
|
|
|
16
16
|
import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed, loadDefaultDir, saveDefaultDir, claudeRecentProjectDir } from './auth-store.mjs'
|
|
17
17
|
import { isSafeToRestart } from './update-gate.mjs'
|
|
18
18
|
import { makeThrottledTrack } from './presence.mjs'
|
|
19
|
+
import { supervisorServes } from './serve-consent.mjs'
|
|
19
20
|
import { resolveServeDir } from './serve-dir.mjs'
|
|
20
21
|
import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
|
|
21
22
|
|
|
@@ -529,26 +530,32 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
529
530
|
return
|
|
530
531
|
}
|
|
531
532
|
let rooms = []
|
|
533
|
+
const me = session.user.id
|
|
532
534
|
try {
|
|
533
|
-
//
|
|
534
|
-
// it
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
535
|
+
// Discovery (Contract C-CODE-2, evolved owner-only → owner-OR-granted): a host
|
|
536
|
+
// bridges the sessions it OWNS *plus* any it has been explicitly granted to serve
|
|
537
|
+
// (code_sessions.serve_grant_uid = me — the owner-consent grant, #206/#208). RLS
|
|
538
|
+
// returns owned OR participant rooms, and a grantee is ALWAYS a participant
|
|
539
|
+
// (set_serve_grant requires it), so a granted row is visible to this login. Without
|
|
540
|
+
// a filter the supervisor would auto-serve rooms you merely JOINED as a guest —
|
|
541
|
+
// spawning your machine into another owner's session that already runs its own
|
|
542
|
+
// bridge; the .or keeps un-granted joins out while letting a consented grant IN.
|
|
543
|
+
// The web watches tpacct:<session-owner>; a granted bridge serves on the owner's
|
|
544
|
+
// behalf (the grant IS the consent), so it is wanted duplication, not stray.
|
|
545
|
+
const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id,serve_grant_uid,participants').or(`owner_id.eq.${me},serve_grant_uid.eq.${me}`).order('last_active_at', { ascending: false }).limit(20)
|
|
540
546
|
rooms = data || []
|
|
541
|
-
// Prune refused entries for rooms we no longer
|
|
542
|
-
// so a transient query error never wipes the set).
|
|
543
|
-
const
|
|
544
|
-
for (const c of [...refused.keys()]) if (!
|
|
547
|
+
// Prune refused entries for rooms we can no longer serve — owned OR granted (only
|
|
548
|
+
// on a SUCCESSFUL fetch, so a transient query error never wipes the set).
|
|
549
|
+
const servableCodes = new Set(rooms.map((r) => r.code))
|
|
550
|
+
for (const c of [...refused.keys()]) if (!servableCodes.has(c)) refused.delete(c)
|
|
545
551
|
} catch { /* transient — keep existing children, retry next tick */ }
|
|
546
552
|
const dirs = loadDirs()
|
|
547
553
|
const served = loadServed()
|
|
548
554
|
for (const r of rooms) {
|
|
549
555
|
const room = r.code
|
|
550
556
|
if (!room || children.has(room)) { if (room) refused.delete(room); continue } // served → not refused
|
|
551
|
-
|
|
557
|
+
// Defense-in-depth behind the .or above: serve iff we OWN the room or hold its grant.
|
|
558
|
+
if (!supervisorServes({ ownerId: r.owner_id, grantUid: r.serve_grant_uid, myUid: me })) continue
|
|
552
559
|
// Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
|
|
553
560
|
// to auto-serve an unbound room from $HOME — that strips the agent's resume context
|
|
554
561
|
// (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
|
package/bridge.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
37
37
|
import { createRequire } from 'node:module'
|
|
38
38
|
import { randomUUID } from 'node:crypto'
|
|
39
39
|
import { createClient } from '@supabase/supabase-js'
|
|
40
|
+
import { serveDecision, amGrantee, standDownOnPeerAnnounce, standDownOnGrantChange, fetchServeRow, refusalMessage } from './serve-consent.mjs'
|
|
40
41
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
41
42
|
import { z } from 'zod'
|
|
42
43
|
import { startClaudeSession } from './claude-session.mjs'
|
|
@@ -569,7 +570,49 @@ async function refreshOwnerPlan() {
|
|
|
569
570
|
if (rows?.[0]?.plan) ownerPlan = rows[0].plan === 'plus' ? 'plus' : 'free'
|
|
570
571
|
} catch { /* DNS-flaky / offline — keep the safe default; the next token refresh retries */ }
|
|
571
572
|
}
|
|
572
|
-
|
|
573
|
+
// ── Room-serve owner consent (Contract C-CODE-3) ──────────────────────────────
|
|
574
|
+
// A room is served by its OWNER's bridge; a non-owner may serve only with the
|
|
575
|
+
// owner's explicit grant (code_sessions.serve_grant_uid). Enforced HERE — before the
|
|
576
|
+
// realtime channel is even built — so a refused bridge never dials the room. Owner
|
|
577
|
+
// reclaim (a grantee yields when the owner's bridge reappears) + grant revocation are
|
|
578
|
+
// wired below on the EXISTING room channel / REST row — no new topic
|
|
579
|
+
// (feedback_no_duplicate_realtime_channel, H41). LIMITATION: enforcement is in-bridge;
|
|
580
|
+
// an OLDER bridge bypasses it (no wire-level lock). Spec: docs/specs/2026-07-04-room-serve-owner-consent.md.
|
|
581
|
+
let roomOwnerId = null // code_sessions.owner_id, learned at the gate
|
|
582
|
+
let myServeUid = null // this bridge's authed uid (null = anon)
|
|
583
|
+
let iServeAsGrantee = false // serving by grant, not as owner → the only bridge that yields
|
|
584
|
+
let yielding = false // set once when standing down, so reclaim + revoke don't double-fire
|
|
585
|
+
{
|
|
586
|
+
// Standalone room mode ran anon unless TP_ACCESS_TOKEN was in env; load the stored
|
|
587
|
+
// login so `npx thinkpool-pair <CODE>` authenticates AS the signed-in user (the
|
|
588
|
+
// identity the consent check needs). Account children already carry the env token.
|
|
589
|
+
if (!codeAuthToken) {
|
|
590
|
+
try { const { loadAuth } = await import('./auth-store.mjs'); codeAuthToken = loadAuth()?.access_token || null } catch { /* no store — stays anon */ }
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
if (codeAuthToken) {
|
|
594
|
+
const { data } = await supabase.auth.getUser(codeAuthToken)
|
|
595
|
+
myServeUid = data?.user?.id || null
|
|
596
|
+
}
|
|
597
|
+
const { ownerId, grantUid, name } = await fetchServeRow({ supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, room })
|
|
598
|
+
roomOwnerId = ownerId
|
|
599
|
+
const decision = serveDecision({ ownerId, grantUid, myUid: myServeUid })
|
|
600
|
+
if (!decision.serve) {
|
|
601
|
+
process.stderr.write(refusalMessage({ room, name, reason: decision.reason }))
|
|
602
|
+
// exit 0 — an intentional stand-down, not a crash, so a supervisor does not
|
|
603
|
+
// thrash-restart it (matches the machine-lock stand-down at the top of boot).
|
|
604
|
+
process.exit(0)
|
|
605
|
+
}
|
|
606
|
+
iServeAsGrantee = amGrantee({ ownerId, grantUid, myUid: myServeUid })
|
|
607
|
+
if (iServeAsGrantee) process.stderr.write(`\n ◆ serving room ${room} by the owner's grant — will yield if the owner's bridge returns or the grant is revoked.\n`)
|
|
608
|
+
} catch (e) {
|
|
609
|
+
// Fail OPEN on a network/transport error ONLY (spec: never strand a user on a
|
|
610
|
+
// flaky fetch). A clean "no row / not granted" is a decision above, not a throw.
|
|
611
|
+
process.stderr.write(`\n ◇ serve-consent check degraded (${e.message}); proceeding without it.\n`)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
refreshOwnerPlan() // fire-and-forget on boot (codeAuthToken now set from env OR the stored login)
|
|
573
616
|
|
|
574
617
|
// ── Pre-flight budget caps — spec docs/specs/2026-07-01-budget-caps.md (nexos steal #1).
|
|
575
618
|
// The bridge calls the durable ledger RPCs as the OWNER (codeAuthToken); is_session_member
|
|
@@ -800,6 +843,12 @@ const announce = () =>
|
|
|
800
843
|
// Additive top-level field; consumed by src/pages/code/room.jsx onAnnounce in
|
|
801
844
|
// a later lane. Older clients ignore it.
|
|
802
845
|
host,
|
|
846
|
+
// uid: the authed user id whose bridge is serving this room (owner or grantee).
|
|
847
|
+
// Additive field (older clients ignore it). A grantee bridge watches peer `bridge`
|
|
848
|
+
// announces for uid === owner_id to know the owner's bridge reclaimed the room and
|
|
849
|
+
// it must stand down (Contract C-CODE-3 invariant 3). No new topic — rides the
|
|
850
|
+
// existing announce (feedback_no_duplicate_realtime_channel).
|
|
851
|
+
uid: myServeUid || undefined,
|
|
803
852
|
// provider: the LLM endpoint this bridge drives Claude Code through —
|
|
804
853
|
// 'anthropic' (the default, the host's regular Claude login) or 'custom'
|
|
805
854
|
// (any Anthropic-compatible base url: GLM/Z.ai, OpenRouter, a proxy). Shown
|
|
@@ -841,6 +890,21 @@ const announce = () =>
|
|
|
841
890
|
],
|
|
842
891
|
})
|
|
843
892
|
|
|
893
|
+
// Owner reclaim (Contract C-CODE-3 invariant 3): a grantee-served bridge that sees
|
|
894
|
+
// the OWNER's bridge announce on this same room channel stands down — the owner's
|
|
895
|
+
// bridge always wins. Reuses the existing `bridge` announce (its additive `uid`) — no
|
|
896
|
+
// new topic (feedback_no_duplicate_realtime_channel). The owner's own bridge never
|
|
897
|
+
// yields (standDownOnPeerAnnounce returns false for it). Clean shutdown(0): shuttingDown
|
|
898
|
+
// suppresses the term-exit broadcast, so no structured terminal row is reaped
|
|
899
|
+
// (docs/specs/2026-07-04-structured-terminal-durability.md — a handover must not delete rows).
|
|
900
|
+
function onPeerBridgeAnnounce (payload) {
|
|
901
|
+
if (yielding) return
|
|
902
|
+
if (!standDownOnPeerAnnounce({ myUid: myServeUid, ownerId: roomOwnerId, peerUid: payload?.uid })) return
|
|
903
|
+
yielding = true
|
|
904
|
+
process.stderr.write(`\n ◆ owner's bridge reconnected — yielding room ${room}.\n`)
|
|
905
|
+
shutdown(0)
|
|
906
|
+
}
|
|
907
|
+
|
|
844
908
|
// ── Thinkpool Ensemble — cross-ROOM reach (Tier 1: same-machine pair peek) ────
|
|
845
909
|
// The in-room Ensemble (read_terminal over sibling terminals) expanded ACROSS the
|
|
846
910
|
// account's OTHER rooms. The account supervisor (account.mjs) is the router: this
|
|
@@ -2478,6 +2542,9 @@ channel
|
|
|
2478
2542
|
announce()
|
|
2479
2543
|
})
|
|
2480
2544
|
.on('broadcast', { event: 'who' }, announce)
|
|
2545
|
+
// Peer bridge announces (owner reclaim) — a grantee yields when the owner's bridge
|
|
2546
|
+
// reappears. broadcast self:false, so this only fires for OTHER bridges on the topic.
|
|
2547
|
+
.on('broadcast', { event: 'bridge' }, ({ payload }) => onPeerBridgeAnnounce(payload))
|
|
2481
2548
|
.subscribe(status => {
|
|
2482
2549
|
if (status === 'SUBSCRIBED') {
|
|
2483
2550
|
realtimeHealthy = true; brokenSince = 0
|
|
@@ -2844,6 +2911,26 @@ setInterval(() => {
|
|
|
2844
2911
|
}
|
|
2845
2912
|
}, 15000).unref()
|
|
2846
2913
|
|
|
2914
|
+
// Grant revocation mid-serve (Contract C-CODE-3 invariant 3): a grantee re-checks the
|
|
2915
|
+
// row on a slow cadence — a cheap poll on the EXISTING REST endpoint, no new realtime
|
|
2916
|
+
// subscription (the owner-reclaim announce path above is the fast one). If the grant no
|
|
2917
|
+
// longer names this bridge (cleared or reassigned), stand down. Owner bridges never poll
|
|
2918
|
+
// (iServeAsGrantee is false for them). A flaky fetch never yields — next tick re-checks.
|
|
2919
|
+
if (iServeAsGrantee) {
|
|
2920
|
+
const RECHECK_MS = Math.max(15000, parseInt(process.env.TP_SERVE_RECHECK_MS, 10) || 30000)
|
|
2921
|
+
setInterval(async () => {
|
|
2922
|
+
if (yielding) return
|
|
2923
|
+
try {
|
|
2924
|
+
const { grantUid } = await fetchServeRow({ supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, room })
|
|
2925
|
+
if (standDownOnGrantChange({ myUid: myServeUid, ownerId: roomOwnerId, grantUid })) {
|
|
2926
|
+
yielding = true
|
|
2927
|
+
process.stderr.write(`\n ◆ serve grant revoked — yielding room ${room}.\n`)
|
|
2928
|
+
shutdown(0)
|
|
2929
|
+
}
|
|
2930
|
+
} catch { /* transient — never yield on a flaky fetch; next tick re-checks */ }
|
|
2931
|
+
}, RECHECK_MS).unref()
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2847
2934
|
// ── auto-update (OS-service tier only) ─────────────────────────────────
|
|
2848
2935
|
// launchd (KeepAlive) and systemd (Restart=always) re-run
|
|
2849
2936
|
// `npx -y thinkpool-pair@latest` on every (re)start, so a clean exit here makes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.159",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"flow-redispatch.mjs",
|
|
32
32
|
"flow-skill-registry.mjs",
|
|
33
33
|
"serve-dir.mjs",
|
|
34
|
+
"serve-consent.mjs",
|
|
34
35
|
"service.mjs",
|
|
35
36
|
"presence.mjs",
|
|
36
37
|
"account.mjs",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Room-serve owner consent — decision core (pure) + row fetch.
|
|
2
|
+
//
|
|
3
|
+
// Spec: docs/specs/2026-07-04-room-serve-owner-consent.md (Contract C-CODE-3).
|
|
4
|
+
// A room is served by its OWNER's bridge; a non-owner bridge may serve only with
|
|
5
|
+
// the owner's explicit grant (`code_sessions.serve_grant_uid`); the owner's bridge
|
|
6
|
+
// always wins reclaim, the grantee stands down.
|
|
7
|
+
//
|
|
8
|
+
// This module holds ONLY pure, order-independent decisions (unit-tested in
|
|
9
|
+
// bridge/test-serve-consent.mjs) plus a single network helper. The wiring —
|
|
10
|
+
// where the gate runs, when a grantee yields — lives in bridge.mjs. Keeping the
|
|
11
|
+
// logic pure is the feedback_code_seed_order_independent discipline: no decision
|
|
12
|
+
// depends on live channel/session state, so every branch is testable off-wire.
|
|
13
|
+
//
|
|
14
|
+
// LIMITATION (documented, spec Contract C-CODE-3 invariant 1): enforcement is
|
|
15
|
+
// in-bridge. A bridge OLDER than this version never reads the column and bypasses
|
|
16
|
+
// the check — there is no server-side wire-level lock. The grant is a coordination
|
|
17
|
+
// signal honored by compliant bridges, not a serve token. Out of scope here.
|
|
18
|
+
|
|
19
|
+
// serveDecision — may THIS bridge serve THIS room?
|
|
20
|
+
// ownerId : code_sessions.owner_id (uuid | null)
|
|
21
|
+
// grantUid : code_sessions.serve_grant_uid (uuid | null) — the one granted non-owner
|
|
22
|
+
// myUid : this bridge's authed user id (uuid | null) — null when the bridge is anon
|
|
23
|
+
// Returns { serve, reason }:
|
|
24
|
+
// reason 'owner' — myUid owns the room (always serves)
|
|
25
|
+
// reason 'grant' — myUid is the granted non-owner
|
|
26
|
+
// reason 'no-identity' — bridge has no authed uid, so it can be neither owner nor
|
|
27
|
+
// grantee → refuse (the new model has no anon serve path)
|
|
28
|
+
// reason 'not-granted' — authed, but neither owner nor the current grantee → refuse
|
|
29
|
+
// reason 'unknown-room'— no owner_id resolved (row missing / RLS-hidden) → refuse
|
|
30
|
+
export function serveDecision ({ ownerId, grantUid, myUid }) {
|
|
31
|
+
if (!myUid) return { serve: false, reason: 'no-identity' }
|
|
32
|
+
if (!ownerId) return { serve: false, reason: 'unknown-room' }
|
|
33
|
+
if (myUid === ownerId) return { serve: true, reason: 'owner' }
|
|
34
|
+
if (grantUid && myUid === grantUid) return { serve: true, reason: 'grant' }
|
|
35
|
+
return { serve: false, reason: 'not-granted' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// supervisorServes — the account supervisor's per-row discovery decision: may this
|
|
39
|
+
// login auto-serve THIS discovered room? True iff the login OWNS the room or HOLDS its
|
|
40
|
+
// serve grant. This is the defense-in-depth per-row check behind the supervisor's
|
|
41
|
+
// `.or(owner_id.eq.me,serve_grant_uid.eq.me)` query — a broad RLS row (owned OR merely
|
|
42
|
+
// participant) must still resolve to owned-or-granted before a child is spawned. Mirrors
|
|
43
|
+
// serveDecision's owner|grant serve set, evaluated against a discovery row rather than a
|
|
44
|
+
// gate fetch (Contract C-CODE-2, evolved owner-only → owner-or-granted).
|
|
45
|
+
export function supervisorServes ({ ownerId, grantUid, myUid }) {
|
|
46
|
+
if (!myUid) return false
|
|
47
|
+
return ownerId === myUid || grantUid === myUid
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// amGrantee — this bridge serves by grant (not as owner). Grantees are the only
|
|
51
|
+
// bridges that ever stand down; the owner's bridge never yields.
|
|
52
|
+
export function amGrantee ({ ownerId, grantUid, myUid }) {
|
|
53
|
+
return !!(myUid && myUid !== ownerId && grantUid && myUid === grantUid)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// standDownOnPeerAnnounce — a grantee-served bridge saw ANOTHER bridge announce on
|
|
57
|
+
// the room channel. Yield iff that peer is the OWNER's bridge (peerUid === ownerId)
|
|
58
|
+
// and I am not the owner. The owner's own bridge (myUid === ownerId) never yields —
|
|
59
|
+
// serveDecision already refused any second non-owner, so a peer that isn't the owner
|
|
60
|
+
// is either a stale duplicate of me or nothing to cede to.
|
|
61
|
+
export function standDownOnPeerAnnounce ({ myUid, ownerId, peerUid }) {
|
|
62
|
+
if (!myUid || !ownerId) return false
|
|
63
|
+
if (myUid === ownerId) return false // owner never stands down
|
|
64
|
+
return !!peerUid && peerUid === ownerId // owner's bridge reclaimed → yield
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// standDownOnGrantChange — a grantee re-checked the row and the grant no longer
|
|
68
|
+
// names it (revoked, or reassigned to a third party). Yield. The owner never yields
|
|
69
|
+
// on a grant change (the grant does not govern the owner).
|
|
70
|
+
export function standDownOnGrantChange ({ myUid, ownerId, grantUid }) {
|
|
71
|
+
if (!myUid || myUid === ownerId) return false
|
|
72
|
+
return grantUid !== myUid
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// fetchServeRow — read the room's consent columns with whatever identity the bridge
|
|
76
|
+
// holds (authed token → RLS returns the row if the user is a member; anon → empty).
|
|
77
|
+
// THROWS on a network/transport error so the caller can fail OPEN (never strand a
|
|
78
|
+
// user on a flaky fetch). A clean HTTP response that simply yields no row is NOT a
|
|
79
|
+
// throw — it returns { ownerId: null } and the caller refuses (unknown room).
|
|
80
|
+
export async function fetchServeRow ({ supabaseUrl, anonKey, token, room }) {
|
|
81
|
+
const headers = { apikey: anonKey }
|
|
82
|
+
if (token) headers.Authorization = `Bearer ${token}`
|
|
83
|
+
const r = await fetch(
|
|
84
|
+
`${supabaseUrl}/rest/v1/code_sessions?select=owner_id,serve_grant_uid,name&code=eq.${encodeURIComponent(room)}`,
|
|
85
|
+
{ headers },
|
|
86
|
+
)
|
|
87
|
+
if (!r.ok) throw new Error(`code_sessions fetch ${r.status}`)
|
|
88
|
+
const rows = await r.json()
|
|
89
|
+
const row = Array.isArray(rows) ? rows[0] : null
|
|
90
|
+
return {
|
|
91
|
+
ownerId: row?.owner_id ?? null,
|
|
92
|
+
grantUid: row?.serve_grant_uid ?? null,
|
|
93
|
+
name: row?.name ?? null,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// refusalMessage — the human-facing stderr line when a bridge refuses to serve.
|
|
98
|
+
export function refusalMessage ({ room, name, reason }) {
|
|
99
|
+
const label = name ? `"${name}"` : '(unnamed)'
|
|
100
|
+
if (reason === 'no-identity') {
|
|
101
|
+
return `\n ◇ Refusing to serve room ${room} — this bridge is not signed in.\n` +
|
|
102
|
+
` A room is served by its owner's bridge. Sign in first: npx thinkpool-pair login\n`
|
|
103
|
+
}
|
|
104
|
+
return `\n ◇ Refusing to serve room ${room} ${label} — it is owned by another person; their bridge serves it.\n` +
|
|
105
|
+
` Ask them to grant your machine hosting rights in the room (bridge status card),\n` +
|
|
106
|
+
` then re-run. The grant is the only way in — there is no override flag.\n`
|
|
107
|
+
}
|