nexo-brain 7.17.6 → 7.17.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.17.6",
3
+ "version": "7.17.8",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,13 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.17.6` is the current packaged-runtime line. Patch release over v7.17.5 - cron health diagnostics are clearer for macOS TCC approval, and catch-up fallback executions now stay visible in `cron_runs` even on legacy or partially migrated runtimes.
21
+ Version `7.17.8` is the current packaged-runtime line. Patch release over v7.17.7 - standalone `nexo chat` now surfaces macOS Full Disk Access guidance, and Brain clears stale permission state after a live access probe succeeds.
22
+
23
+ Previously in `7.17.7`: patch release over v7.17.6 - macOS TCC privacy denials now become a guided Full Disk Access permission state instead of an unexplained cron failure, with Desktop-ready status written for user action.
24
+
25
+ Previously in `7.17.6`: patch release over v7.17.5 - cron health diagnostics are clearer for macOS TCC approval, and catch-up fallback executions now stay visible in `cron_runs` even on legacy or partially migrated runtimes.
26
+
27
+ Previously in `7.17.5`: patch release over v7.17.4 - `nexo --version --json` now returns machine-readable update status so NEXO Desktop can populate the Updates panel without scraping slower human output.
22
28
 
23
29
  Previously in `7.17.4`: corrective patch over v7.17.3 - automation runners now keep full NEXO discipline for real background agents while strict JSON children stay clean, and runtime doctor/metrics expose caller coverage and Guardian injection telemetry instead of hiding blind spots.
24
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.17.6",
3
+ "version": "7.17.8",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
package/src/cli.py CHANGED
@@ -2187,6 +2187,29 @@ def _prompt_for_terminal_client(
2187
2187
  print("Invalid choice. Try again.", file=sys.stderr)
2188
2188
 
2189
2189
 
2190
+ def _ensure_chat_full_disk_access_notice(*, interactive: bool | None = None) -> dict:
2191
+ runtime_power = _load_runtime_power_support()
2192
+ if interactive is None:
2193
+ interactive = bool(getattr(sys.stdin, "isatty", lambda: False)())
2194
+ try:
2195
+ choice = runtime_power["ensure_full_disk_access_choice"](
2196
+ interactive=interactive,
2197
+ reason="chat",
2198
+ output_fn=lambda message: print(message, file=sys.stderr),
2199
+ )
2200
+ except EOFError:
2201
+ return {
2202
+ "status": "later",
2203
+ "prompted": False,
2204
+ "reasons": [],
2205
+ "message": "Full Disk Access setup skipped because stdin closed.",
2206
+ }
2207
+ message = str(choice.get("message") or "").strip()
2208
+ if message and (choice.get("prompted") or choice.get("relevant")):
2209
+ print(f"[NEXO] Full Disk Access: {message}", file=sys.stderr)
2210
+ return choice
2211
+
2212
+
2190
2213
  def _chat(args):
2191
2214
  target = args.path or "."
2192
2215
  selected_client = getattr(args, "client", None)
@@ -2211,6 +2234,11 @@ def _chat(args):
2211
2234
  except Exception:
2212
2235
  pass
2213
2236
 
2237
+ try:
2238
+ _ensure_chat_full_disk_access_notice()
2239
+ except Exception as exc:
2240
+ print(f"[NEXO] Full Disk Access check warning: {exc}", file=sys.stderr)
2241
+
2214
2242
  try:
2215
2243
  from client_preferences import (
2216
2244
  detect_installed_clients,
@@ -49,6 +49,7 @@ FULL_DISK_ACCESS_UNSET = "unset"
49
49
  FULL_DISK_ACCESS_GRANTED = "granted"
50
50
  FULL_DISK_ACCESS_DECLINED = "declined"
51
51
  FULL_DISK_ACCESS_LATER = "later"
52
+ FULL_DISK_ACCESS_STATE_FILE = NEXO_HOME / "runtime" / "state" / "full-disk-access-required.json"
52
53
  VALID_FULL_DISK_ACCESS_STATUSES = {
53
54
  FULL_DISK_ACCESS_UNSET,
54
55
  FULL_DISK_ACCESS_GRANTED,
@@ -550,7 +551,12 @@ def _tail_has_permission_denial(log_file: Path) -> bool:
550
551
  size = fh.tell()
551
552
  fh.seek(max(size - 4096, 0))
552
553
  tail = fh.read().decode("utf-8", errors="ignore")
553
- return "Operation not permitted" in tail
554
+ lowered = tail.lower()
555
+ return (
556
+ "operation not permitted" in lowered
557
+ or "authorization denied" in lowered
558
+ or "unable to open database" in lowered and "com.apple.tcc/tcc.db" in lowered
559
+ )
554
560
  except Exception:
555
561
  return False
556
562
 
@@ -568,10 +574,12 @@ def detect_full_disk_access_reasons(*, system: str | None = None) -> list[str]:
568
574
 
569
575
  logs_dir = paths.logs_dir()
570
576
  if logs_dir.is_dir():
571
- for log_file in sorted(logs_dir.glob("*-stderr.log")):
577
+ log_files = list(logs_dir.glob("*-stderr.log"))
578
+ log_files.extend(logs_dir.glob("tcc-auto-approve.log"))
579
+ for log_file in sorted(log_files):
572
580
  if _tail_has_permission_denial(log_file):
573
581
  reasons.append(
574
- f"Recent background job stderr hit 'Operation not permitted' ({log_file.name})"
582
+ f"Recent background job hit a macOS privacy denial ({log_file.name})"
575
583
  )
576
584
  break
577
585
  return reasons
@@ -787,14 +795,25 @@ def set_power_policy(policy: str) -> dict:
787
795
 
788
796
  def set_full_disk_access_status(status: str, *, reasons: list[str] | None = None) -> dict:
789
797
  schedule = load_schedule_config()
790
- schedule[FULL_DISK_ACCESS_STATUS_KEY] = normalize_full_disk_access_status(status)
798
+ normalized = normalize_full_disk_access_status(status)
799
+ schedule[FULL_DISK_ACCESS_STATUS_KEY] = normalized
791
800
  schedule[FULL_DISK_ACCESS_STATUS_VERSION_KEY] = FULL_DISK_ACCESS_STATUS_VERSION
801
+ if normalized == FULL_DISK_ACCESS_GRANTED:
802
+ reasons = []
803
+ clear_full_disk_access_required_state()
792
804
  if reasons is not None:
793
805
  schedule[FULL_DISK_ACCESS_REASONS_KEY] = normalize_full_disk_access_reasons(reasons)
794
806
  save_schedule_config(schedule)
795
807
  return schedule
796
808
 
797
809
 
810
+ def clear_full_disk_access_required_state() -> None:
811
+ try:
812
+ FULL_DISK_ACCESS_STATE_FILE.unlink(missing_ok=True)
813
+ except Exception:
814
+ pass
815
+
816
+
798
817
  def prompt_for_power_policy(
799
818
  *,
800
819
  reason: str = "install",
@@ -931,6 +950,9 @@ def ensure_full_disk_access_choice(
931
950
  )
932
951
 
933
952
  schedule[FULL_DISK_ACCESS_STATUS_KEY] = status
953
+ if status == FULL_DISK_ACCESS_GRANTED:
954
+ schedule[FULL_DISK_ACCESS_REASONS_KEY] = []
955
+ clear_full_disk_access_required_state()
934
956
  save_schedule_config(schedule)
935
957
  return {
936
958
  "status": status,
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env python3
2
+ """First-response jargon checker (Communication Guardrail enforcement).
3
+
4
+ Implements followup NF-DS-32D6E12E: a linter that scans the FIRST visible
5
+ message the agent emits in a turn for NEXO internal jargon that violates
6
+ the Communication Guardrail in CLAUDE.md.
7
+
8
+ Token list (case-insensitive substring match), taken verbatim from the
9
+ followup spec:
10
+
11
+ Learning #, protocol debt, cortex eval, runtime-core, guard_check,
12
+ heartbeat, pre-emptive guard, enforcer, task_open, task_close, NF-
13
+
14
+ Use as:
15
+
16
+ * Library:
17
+ from src.scripts.jargon_first_response import scan_text, register_debt_if_violations
18
+
19
+ * CLI (manual review of a recent reply):
20
+ echo "Learning #42 applied via task_open" \
21
+ | python3 src/scripts/jargon_first_response.py --stdin --session-id NEXO-SID
22
+
23
+ * Future hook integration (PostToolUse / Stop) loads the transcript with
24
+ `transcript_utils.load_transcript` and pipes the latest assistant
25
+ message through `register_debt_if_violations`. Hook wiring is left as
26
+ a separate change so the linter ships independently testable.
27
+
28
+ Exit codes (CLI):
29
+ 0 — no violations detected (or only allowed because user requested detail)
30
+ 1 — at least one prohibited token found
31
+ 2 — invocation error (missing input)
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ import os
37
+ import re
38
+ import sys
39
+ from pathlib import Path
40
+ from typing import Iterable, List, Sequence
41
+
42
+ # Order matters only for stable output. Longer / multi-word phrases first
43
+ # so the snippet boundary is clearer in the matches view.
44
+ PROHIBITED_TOKENS: Sequence[str] = (
45
+ "Learning #",
46
+ "protocol debt",
47
+ "cortex eval",
48
+ "runtime-core",
49
+ "guard_check",
50
+ "pre-emptive guard",
51
+ "enforcer",
52
+ "task_open",
53
+ "task_close",
54
+ "heartbeat",
55
+ "NF-",
56
+ )
57
+
58
+ # Heuristic signals from the *user* message that mean "operator asked for
59
+ # technical detail, jargon is allowed in the reply". The first answer
60
+ # only counts as a guardrail violation when none of these are present.
61
+ _USER_DETAIL_PATTERNS = (
62
+ re.compile(r"\bdebugg(?:ing|er)?\b", re.IGNORECASE),
63
+ re.compile(r"\bstack\s*trace\b", re.IGNORECASE),
64
+ re.compile(r"\b(?:protocol\s+debt|guard_check|task_open|task_close|enforcer|heartbeat)\b", re.IGNORECASE),
65
+ re.compile(r"\b(?:nf-[a-z0-9-]+|learning\s+#?\d+)\b", re.IGNORECASE),
66
+ re.compile(r"\b(internal|runtime|architecture|cortex|drive)\b", re.IGNORECASE),
67
+ re.compile(r"\b(?:explica|explain|deep[\s-]*dive|d[ée]tailled|d[ée]tail)\b", re.IGNORECASE),
68
+ )
69
+
70
+
71
+ def _first_visible_paragraph(text: str) -> str:
72
+ """Return the first ~600 chars of operator-visible text.
73
+
74
+ Strips leading whitespace, blank lines, and ignores fenced code blocks
75
+ at the very top (release notes / code-only first messages are not
76
+ "first response prose" for the guardrail purpose).
77
+ """
78
+ if not text:
79
+ return ""
80
+ cleaned = text.lstrip()
81
+ if cleaned.startswith("```"):
82
+ end = cleaned.find("```", 3)
83
+ if end != -1:
84
+ cleaned = cleaned[end + 3 :].lstrip()
85
+ # First two paragraphs is usually plenty for the linter; longer
86
+ # replies have time to recover into plain language.
87
+ paragraphs = re.split(r"\n\s*\n", cleaned)
88
+ head = "\n\n".join(paragraphs[:2])
89
+ return head[:600]
90
+
91
+
92
+ def user_requested_detail(user_message: str) -> bool:
93
+ """True if the operator explicitly asked for technical / NEXO-internal detail."""
94
+ if not user_message:
95
+ return False
96
+ return any(pat.search(user_message) for pat in _USER_DETAIL_PATTERNS)
97
+
98
+
99
+ def scan_text(text: str, *, tokens: Iterable[str] = PROHIBITED_TOKENS) -> List[dict]:
100
+ """Return a list of `{token, index, snippet}` for each prohibited match."""
101
+ if not text:
102
+ return []
103
+ target = _first_visible_paragraph(text)
104
+ if not target:
105
+ return []
106
+ lowered = target.lower()
107
+ found: List[dict] = []
108
+ for token in tokens:
109
+ needle = token.lower()
110
+ start = 0
111
+ while True:
112
+ idx = lowered.find(needle, start)
113
+ if idx < 0:
114
+ break
115
+ snippet_start = max(0, idx - 25)
116
+ snippet_end = min(len(target), idx + len(token) + 25)
117
+ found.append({
118
+ "token": token,
119
+ "index": idx,
120
+ "snippet": target[snippet_start:snippet_end].replace("\n", " "),
121
+ })
122
+ start = idx + max(1, len(needle))
123
+ found.sort(key=lambda row: row["index"])
124
+ return found
125
+
126
+
127
+ def register_debt_if_violations(
128
+ session_id: str,
129
+ assistant_text: str,
130
+ *,
131
+ user_message: str = "",
132
+ task_id: str = "",
133
+ evidence_prefix: str = "",
134
+ ) -> dict:
135
+ """Register a protocol_debt of type `communication_guardrail` when the
136
+ first response uses prohibited NEXO jargon and the user did NOT ask
137
+ for technical detail.
138
+
139
+ Returns a result dict with keys:
140
+ - `violations`: list of matches from `scan_text`
141
+ - `skipped`: bool — true if checker did not register (no SID, user
142
+ requested detail, or no violations)
143
+ - `debt_id`: int|None — id of the created debt, when applicable
144
+ - `reason`: short human-readable status
145
+ """
146
+ matches = scan_text(assistant_text)
147
+ if not matches:
148
+ return {"violations": [], "skipped": True, "debt_id": None, "reason": "no_violations"}
149
+ if user_requested_detail(user_message):
150
+ return {"violations": matches, "skipped": True, "debt_id": None, "reason": "user_requested_detail"}
151
+ sid = (session_id or "").strip()
152
+ if not sid:
153
+ return {"violations": matches, "skipped": True, "debt_id": None, "reason": "missing_session_id"}
154
+ try:
155
+ # Local import keeps this module importable in isolated test
156
+ # contexts (no NEXO DB) when callers only need `scan_text`.
157
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
158
+ from db import create_protocol_debt # type: ignore
159
+ except Exception as err: # pragma: no cover — defensive in tests
160
+ return {"violations": matches, "skipped": True, "debt_id": None, "reason": f"db_unavailable:{err}"}
161
+ evidence_lines = [evidence_prefix] if evidence_prefix else []
162
+ for match in matches[:5]:
163
+ evidence_lines.append(f"- '{match['token']}' @ {match['index']}: ...{match['snippet']}...")
164
+ evidence = "\n".join(line for line in evidence_lines if line)[:4000]
165
+ debt = create_protocol_debt(
166
+ sid,
167
+ "communication_guardrail",
168
+ severity="warn",
169
+ task_id=task_id,
170
+ evidence=evidence,
171
+ )
172
+ return {
173
+ "violations": matches,
174
+ "skipped": False,
175
+ "debt_id": debt.get("id") if isinstance(debt, dict) else None,
176
+ "reason": "debt_registered",
177
+ }
178
+
179
+
180
+ def _read_text_from_args(args: argparse.Namespace) -> str:
181
+ if args.text is not None:
182
+ return args.text
183
+ if args.stdin:
184
+ return sys.stdin.read()
185
+ if args.file:
186
+ return Path(args.file).read_text(encoding="utf-8")
187
+ return ""
188
+
189
+
190
+ def main(argv: Sequence[str] | None = None) -> int:
191
+ parser = argparse.ArgumentParser(description="First-response jargon checker (NEXO Communication Guardrail).")
192
+ parser.add_argument("--text", help="Assistant text to scan (inline).")
193
+ parser.add_argument("--stdin", action="store_true", help="Read assistant text from stdin.")
194
+ parser.add_argument("--file", help="Read assistant text from file.")
195
+ parser.add_argument("--user-message", default="", help="Optional user message preceding the reply.")
196
+ parser.add_argument("--session-id", default=os.environ.get("NEXO_SID", ""), help="Session ID for protocol_debt registration.")
197
+ parser.add_argument("--task-id", default="", help="Optional task ID to attach to the debt.")
198
+ parser.add_argument("--register-debt", action="store_true", help="Register a protocol_debt when violations are found.")
199
+ parser.add_argument("--evidence-prefix", default="", help="Optional prefix for the evidence string.")
200
+ args = parser.parse_args(argv)
201
+
202
+ text = _read_text_from_args(args)
203
+ if not text:
204
+ parser.error("provide --text, --stdin, or --file")
205
+ return 2
206
+
207
+ if args.register_debt:
208
+ result = register_debt_if_violations(
209
+ args.session_id,
210
+ text,
211
+ user_message=args.user_message,
212
+ task_id=args.task_id,
213
+ evidence_prefix=args.evidence_prefix,
214
+ )
215
+ violations = result["violations"]
216
+ else:
217
+ violations = scan_text(text)
218
+ result = {
219
+ "violations": violations,
220
+ "skipped": True,
221
+ "debt_id": None,
222
+ "reason": "dry_run",
223
+ }
224
+
225
+ if not violations:
226
+ print("[jargon-checker] OK — no prohibited tokens in first response.")
227
+ return 0
228
+ print(f"[jargon-checker] {len(violations)} match(es) detected:")
229
+ for match in violations:
230
+ print(f" - {match['token']!r} @ {match['index']} …{match['snippet']}…")
231
+ if result.get("debt_id"):
232
+ print(f"[jargon-checker] protocol_debt registered id={result['debt_id']} reason=communication_guardrail")
233
+ elif args.register_debt:
234
+ print(f"[jargon-checker] no debt registered: {result.get('reason')}")
235
+ return 1
236
+
237
+
238
+ if __name__ == "__main__":
239
+ raise SystemExit(main())
@@ -25,17 +25,99 @@ TCC_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
25
25
  VERSIONS_DIR="$HOME/.local/share/claude/versions"
26
26
  MARKER_DIR="$NEXO_HOME/runtime/data/.tcc-approved"
27
27
  LOG="$NEXO_HOME/runtime/logs/tcc-auto-approve.log"
28
+ FDA_STATE="$NEXO_HOME/runtime/state/full-disk-access-required.json"
28
29
 
29
- mkdir -p "$MARKER_DIR" "$(dirname "$LOG")"
30
+ mkdir -p "$MARKER_DIR" "$(dirname "$LOG")" "$(dirname "$FDA_STATE")"
30
31
 
31
32
  FAILED=0
33
+ FDA_REQUIRED=0
32
34
  APPROVED_VERSIONS=0
33
35
  PYTHON_APPROVED=0
36
+ FDA_REASON="macOS blocked tcc-approve from opening the user TCC database. Grant Full Disk Access to /bin/bash and NEXO Desktop, then retry background permission setup."
34
37
 
35
38
  log_line() {
36
39
  echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG"
37
40
  }
38
41
 
42
+ is_fda_error() {
43
+ local text
44
+ text="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
45
+ [[ "$text" == *"authorization denied"* ]] && return 0
46
+ [[ "$text" == *"operation not permitted"* ]] && return 0
47
+ [[ "$text" == *"unable to open database"* && "$text" == *"com.apple.tcc/tcc.db"* ]] && return 0
48
+ [[ "$text" == *"privacy"* && "$text" == *"com.apple.tcc"* ]] && return 0
49
+ return 1
50
+ }
51
+
52
+ python_for_state() {
53
+ local candidate
54
+ if [ -n "${NEXO_CODE:-}" ]; then
55
+ candidate="$(dirname "$NEXO_CODE")/.venv/bin/python"
56
+ [ -x "$candidate" ] && { echo "$candidate"; return 0; }
57
+ candidate="$(dirname "$NEXO_CODE")/.venv/bin/python3"
58
+ [ -x "$candidate" ] && { echo "$candidate"; return 0; }
59
+ fi
60
+ command -v python3 2>/dev/null || true
61
+ }
62
+
63
+ record_full_disk_access_required() {
64
+ local py
65
+ py="$(python_for_state)"
66
+
67
+ cat > "$FDA_STATE" <<EOF
68
+ {
69
+ "status": "later",
70
+ "source": "tcc-approve",
71
+ "updated_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
72
+ "reasons": [
73
+ "$FDA_REASON"
74
+ ]
75
+ }
76
+ EOF
77
+
78
+ [ -n "$py" ] || return 0
79
+ NEXO_FDA_REASON="$FDA_REASON" NEXO_HOME="$NEXO_HOME" "$py" <<'PY'
80
+ import json
81
+ import os
82
+ from pathlib import Path
83
+
84
+ nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
85
+ reason = os.environ.get("NEXO_FDA_REASON", "").strip()
86
+ targets = [nexo_home / "personal" / "config" / "schedule.json"]
87
+ legacy = nexo_home / "config" / "schedule.json"
88
+ if legacy.exists():
89
+ targets.append(legacy)
90
+
91
+ seen = set()
92
+ for target in targets:
93
+ key = str(target.resolve(strict=False))
94
+ if key in seen:
95
+ continue
96
+ seen.add(key)
97
+ try:
98
+ target.parent.mkdir(parents=True, exist_ok=True)
99
+ data = {}
100
+ if target.exists():
101
+ try:
102
+ parsed = json.loads(target.read_text(encoding="utf-8"))
103
+ if isinstance(parsed, dict):
104
+ data = parsed
105
+ except Exception:
106
+ data = {}
107
+ reasons = data.get("full_disk_access_reasons")
108
+ if not isinstance(reasons, list):
109
+ reasons = []
110
+ if reason and reason not in reasons:
111
+ reasons.append(reason)
112
+ data["full_disk_access_status"] = "later"
113
+ data["full_disk_access_status_version"] = 1
114
+ data["full_disk_access_reasons"] = reasons
115
+ target.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
116
+ except Exception:
117
+ pass
118
+ PY
119
+ }
120
+
39
121
  approve_service() {
40
122
  local svc="$1"
41
123
  local client="$2"
@@ -49,6 +131,9 @@ approve_service() {
49
131
  fi
50
132
 
51
133
  log_line "WARN: failed TCC approval service=$svc client=$client: ${output:-sqlite3 failed}"
134
+ if is_fda_error "${output:-}"; then
135
+ FDA_REQUIRED=1
136
+ fi
52
137
  return 1
53
138
  }
54
139
 
@@ -119,6 +204,13 @@ if [ -n "$NEXO_CODE" ]; then
119
204
  fi
120
205
  fi
121
206
 
207
+ if [ "$FAILED" -ne 0 ] && [ "$FDA_REQUIRED" -ne 0 ]; then
208
+ record_full_disk_access_required
209
+ log_line "Full Disk Access required: $FDA_REASON"
210
+ echo "TCC auto-approve: Full Disk Access required; Desktop will prompt the user"
211
+ exit 0
212
+ fi
213
+
122
214
  if [ "$FAILED" -ne 0 ]; then
123
215
  echo "TCC auto-approve failed; see $LOG" >&2
124
216
  exit 1