mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
fix/opencode-session-header
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f9092668a8 |
fix(agent): stop the tool-schema ratchet meaning a different thing per host (#1017)
* fix(agent): stop the tool-schema ratchet meaning a different thing per host TestEagerToolSchemaTokenBudget counted exec_command shell guidance along with the schemas, and that guidance is host-specific. The same tree measured 3498 on Linux, about 3600 on a Windows host with pwsh 7, and 3653 on Windows PowerShell 5.1, because shellGuidanceForRuntime appends a longer block for 5.1 (it has to explain that && and || do not work there). 5.1 is stock Windows, and that case has been over the 3650 ceiling since view_image landed. So `go test ./...` failed for anyone on a stock Windows box while CI stayed green: the runners have pwsh 7 and take the cheaper branch. The workflow does run the tests on windows-latest, so this was never a coverage gap, just a ceiling sized against whichever shell the runner happened to have. The two halves are now charged separately. The schema ceiling measures the schemas alone and reads 3498 on Linux and on Windows PowerShell 5.1, so it ratchets the same thing everywhere. The host guidance gets its own ceiling at 200, above the worst case rather than above this machine: 5.1 is 155 tokens, pwsh 7 is 110, cmd.exe is 87, POSIX is 0. No guidance text was trimmed. It is what stops the model writing bash on Windows, and the 5.1 note is the reason a chained command does not silently do half its work. Paying 155 tokens for it is the right trade; measuring it as if it were a schema was not. HostExecCommandShellGuidance is exported for the budget, which is production surface added for a test. It is one accessor over existing logic, and the alternative is a ratchet that cannot tell the two costs apart. A guard in internal/tools pins that the guidance really is the tail of the description, so the subtraction cannot silently start measuring the wrong thing. * test(tools,agent): make the guidance contract checkable off Windows The ratchet separates the fixed tool schemas from this host's exec_command shell guidance, and both halves of that separation were only being checked on Windows. The description test returned early off Windows once the accessor reported no guidance, so it never looked at the description. Dropping the GOOS condition in the constructor would have a POSIX host append its own guidance (about 150 characters on darwin) with the accessor still reporting none and the ratchet charging every byte of it to the schemas, and nothing would fail. Both the description and the accessor now take goos, so the contract is checked for all three platforms from any one of them. The budget also went from subtracting a separately rounded guidance estimate to removing the guidance from the description before estimating. ApproxTextTokens is an integer division, so floor((schema+guidance)/4) - floor(guidance/4) is one token high whenever the two remainders add past four, and which side of that a host lands on depends on the length of its guidance. That is the same host-dependence the ratchet was being fixed for, one token smaller. No behaviour change: exec_command's description is byte for byte what it was on every platform. |
||
|
|
518d2e53ba |
fix(windows): use PowerShell for command execution (#804)
* fix(windows): prefer PowerShell for command execution Detect PowerShell 7 or Windows PowerShell before falling back to cmd.exe, and share the selected host shell across agent tools and the interactive shell escape. Configure UTF-8 output, preserve restricted-sandbox MSYS safeguards, and add Windows coverage for pipelines, workspace writes, and interactive stdin sessions. * test(windows): align shell coverage with PowerShell Make Windows command fixtures and guidance assertions follow the selected host shell, preserve cmd fallback coverage, and use a genuine MSYS executable when PowerShell aliases are available. Remove the unsupported non-TTY PowerShell stdin assertion; Windows interactive stdin requires a separate PTY transport. * fix(sandbox): classify Windows executable suffixes Normalize .exe, .cmd, .bat, and .com command names before safety and network lookup so PowerShell invocations such as curl.cmd receive the same network permission handling as curl. Add analyzer and engine-level regressions for the Windows PowerShell command shape observed in CI. * fix(sandbox): normalize absolute Windows command paths Extract drive-letter and UNC basenames before removing shell escape characters so fully qualified Windows executables continue matching canonical safety and network classifications. Cover both drive and UNC curl paths with analyzer regressions. * fix(sandbox): normalize relative Windows paths Recognize drive-relative, explicit relative, and suffixed backslash executable paths before generic shell escape removal so Windows commands continue to match canonical sandbox risk entries. * fix(windows): harden PowerShell execution * fix(sandbox): classify PowerShell removal aliases |
||
|
|
f10ed0c893 |
fix(tools): Windows cmd.exe quoting guidance and clipboard escaping fix (#468)
* merge: rebase onto main, combine Windows shell guidance with #476's MSYS text #476 merged into main and rewrote the same "Shell syntax: Windows cmd.exe..." guidance string in system_prompt.go and shellGuidanceForGOOS in shell_runtime.go that this branch also edits (this PR's fix for the broken single-quoted quoting examples). Combined both into one coherent string in each spot: the double-quote/single-quote metachar rule with correct examples, plus #476's MSYS coreutils warning and require_escalated mention. Merged the corresponding test assertions in loop_test.go the same way. internal/imageinput/clipboard.go, internal/tools/bash_tool_test.go, and internal/tools/exec_command_test.go applied cleanly with no overlap. * test(tools): add host shell syntax description test for exec_command * chore: retrigger CI after infra flake * style(tools): gofmt exec_command_test.go --------- Co-authored-by: Vasanthdev2004 <[email protected]> |
||
|
|
066681855b |
fix(tools): block MSYS and WSL shells under the Windows sandbox (#587)
* fix(tools): block MSYS and WSL shells under the Windows sandbox Git-for-Windows bash.exe and sh.exe are MSYS binaries: the MSYS runtime secures its signal pipe and shared-memory sections with explicit DACLs granting only the user, Administrators, and SYSTEM (sigproc.cc sigproc_init -> sec_user_nih), so under the WRITE_RESTRICTED token the second access check can never pass and startup dies with "couldn't create signal pipe" or "CreateFileMapping <SID>.1", Win32 error 5. The System32 WSL bash launcher fails equivalently: the restricted token cannot connect to the WSL service (Bash/Service/CreateInstance/ E_ACCESSDENIED). Every executable a bare bash can resolve to fails, so block bash and sh preflight like the MSYS coreutils from #476, mention the indirect spawn paths (git hooks, git/gh credential helpers) in the shared suggestion, detect the WSL launcher's UTF-16LE service-denied stderr, and document the root cause in the runner's KNOWN LIMITATION comment beside the Schannel note. * fix(tui): align picker_test switchProviderModel call with 4-value return * test(tools): cover sh.exe in Windows shell blocking test |
||
|
|
8b248f4e11 |
fix(tools): platform-specific pager suggestions, quote/caret-safe cd detection (#543)
less/more/most now suggest `type` instead of cat/head/tail on Windows, since those tools don't exist there. Drop `more` from the MSYS sandbox suggestion too, since more is itself blocked as an interactive pager and would send the model straight into that guard next. detectShellCommandIssue also strips double-quoted spans and cmd.exe caret escapes before matching the bash-style `cd /foo` pattern, so a cd-shaped string inside a quoted argument (e.g. a commit message) isn't mistaken for an actual command. |
||
|
|
81aad58d97 |
fix(tools): Block MSYS coreutils under Windows sandbox (#476)
* fix(tools): block MSYS coreutils under Windows sandbox Git for Windows usr\bin coreutils (head, grep, cat, ...) are MSYS PE binaries that fail with ACCESS_DENIED and cygheap/signal-pipe errors when Zero runs them in the write-restricted Windows sandbox. Detect and block these commands before execution, surface MSYS runtime failures from stderr, steer agents toward native Zero tools or require_escalated, and exclude MSYS-prone names from known-safe command tails. Refs #458 * fix(tools): anchor the last MSYS failure heuristic to MSYS markers CodeRabbit review feedback: the win32-error-5 plus terminating check had no MSYS-specific anchor, so unrelated access-denied failures could be mislabeled as windows_msys_sandbox. Require an MSYS marker like the neighboring checks do. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(tools): let escalation bypass the MSYS guard and make detection quote-aware Addresses jatmn's review on PR #476: - bash and exec_command called detectShellCommandIssue before resolving sandbox_permissions into a command engine, so a require_escalated retry of a blocked command was still hard-blocked by the same preflight check. Resolve the command engine first and only bypass the windows_msys_sandbox issue kind when that resolves to true unsandboxed execution; other issue kinds (real cmd.exe syntax problems) still block regardless of escalation. - Unify all MSYS-prone command name checks (MsysProneCommandName, the preflight scan, and transitively command_prefix.go's known-safe guard) behind one windowsMsysProneNames set. This fixes expr (in the name set but missing from the old regex alternations, so never proactively flagged) and ls (hit the older windows_shell_syntax branch first, so it got different metadata than every other MSYS-prone name). - Make the preflight scan quote-aware: split the command into cmd.exe- operator-separated segments respecting double-quote grouping, and only check the first word of each segment instead of scanning raw text anywhere. Fixes false positives on quoted text like commit messages or PR comment bodies that merely mention a coreutil name. * test(tools): decouple MSYS-guard bypass tests from real command output TestBashToolRequireEscalatedMsysGuard and TestExecCommandRequireEscalatedBypassesMsysGuardAfterApproval asserted shell_issue == "" to prove the require_escalated bypass worked. Once bypassed, "cat somefile.txt" actually runs unsandboxed, and its real, PATH-dependent output could trip the unrelated post-execution detectShellOutputIssue heuristic, failing the assertion for reasons unrelated to the preflight guard under test. Assert on the exit_code "-1" sentinel (set only by shellIssueBlockResult) instead, which directly reflects whether the preflight guard blocked the command. * fix(tools): anchor MSYS path detection to command position, fix prefix approval gap Address jatmn's latest review on PR #476: - shell_runtime.go: the explicit MSYS binary-path check (usr\bin\, mingw64\bin\, msys-2.0.dll, cygwin1.dll) ran against the raw command string, so a quoted mention (e.g. a commit message describing a usr\bin\head.exe failure) was still hard-blocked. It now runs against the first word of each operator-separated segment, same as the coreutil-name check. windowsCommandSegments also stopped treating ; as a command separator (cmd.exe does not split on ;, unlike bash) and now respects cmd.exe's ^ escape character, so escaped operators like echo ^| head are no longer misread as a real head invocation. - command_prefix.go: proposedCommandPrefix could offer to approve a single segment (e.g. "ps aux" in "ps aux | head -5") without checking that every other segment was independently safe. Since approving a prefix escalates the whole command to bypass the sandbox, an MSYS-prone or otherwise unsafe segment could ride along uncovered. It now only proposes a segment as the prefix when every other segment in the command is known-safe on its own. Added regression tests for quoted MSYS path mentions, caret-escaped operators, semicolon text, and the prefix-approval gap. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(tools): treat redirection as a command boundary, stop using command text as output evidence Addresses jatmn's latest review on PR #476: - firstCommandWord only split on whitespace, so cmd.exe redirection attached directly to a command name (head>out.txt, cat<in.txt, grep>matches.txt) was read as one word and never matched against the MSYS-prone name set. It now also stops at < and >. - detectShellOutputIssue scanned command+output together, so a command whose own argument text merely quoted an MSYS failure string (e.g. a gh pr comment --body describing the bug) could be mislabeled after running even though the real stdout/stderr was unrelated. Removed the command parameter entirely from detectShellOutputIssue and both call sites (bash.go, exec_command.go), so the command line can never be used as post-execution evidence again. * fix(tools): remove unused truncateHeadTail wrapper CodeRabbit nitpick: truncateHeadTailWithTotal is called directly everywhere, so this wrapper is dead code that trips golangci-lint's unused check and fails CI. --------- Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
5658a36627 |
fix(tools): flag piped POSIX utilities before running on Windows (#412)
* fix(tools): flag piped POSIX utilities (head/tail/grep/...) before running on Windows Zero always executes the bash tool through native cmd.exe on Windows (shellExecutable() in bash.go), but the pre-flight Windows-syntax detector only caught bash-style `cd /...` and bare `ls`. Anything piped into head/tail/grep/wc/awk/sed/cut/xargs/tr — very common in model-generated commands like `git log ... | head` — ran straight through to cmd.exe, which has no such builtins, wasting a turn on a guaranteed failure. Reported in the wild via `git log ... | head` and a `git blame ... | grep` pipeline. detectShellCommandIssue now also flags these before the command runs, with the same "translate to cmd.exe / use Zero's native tools" guidance already used for the cd/ls cases. Note: the existing detectShellOutputIssue path (matching cmd.exe's error text post-execution) only recognizes English strings and will miss this on non-English Windows locales — the pre-flight check above is locale-independent since it inspects the model-generated command text rather than cmd.exe's translated error output. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(tools): require whitespace/end after POSIX utility name, not just \b CodeRabbit review on PR #412: windowsPosixUtilityPattern used \b after the utility alternation, but \b only requires a non-word character next — so tail-log.sh, grep-cli, and sed.exe would all false-positive (the hyphen/dot satisfies \b without being a real command boundary). Match windowsLSCommandPattern's existing convention: require whitespace or end-of-string after the utility name instead. Co-Authored-By: Claude Sonnet 5 <[email protected]> --------- Co-authored-by: Claude Sonnet 5 <[email protected]> |
||
|
|
b776a9281f |
fix(sandbox): grant ancestor metadata so cd into the workspace works (#302)
* fix(sandbox): grant ancestor metadata so cd into the workspace works macOS seatbelt granted file-read* on each read root via (subpath "<root>"), which covers the root and its descendants but NOT its parent directories. An absolute cd (and any path the kernel canonicalises) must stat each ancestor to traverse to a deeply-nested workspace like /Users/me/Downloads/app — those parents weren't granted, so seatbelt denied the chdir and surfaced it as the misleading 'cd: <path>: Not a directory' (ENOTDIR). This blocked the agent from cd-ing into its own workspace (e.g. 'cd /abs/workspace && go test'). Fix: grant file-read-metadata + file-test-existence on the path-ancestors of each read root (metadata only — ancestor contents stay unreadable). Same pattern already used for /opt/homebrew, /usr/local, /System/Volumes/Data/private. Verified end-to-end: 'cd /abs/workspace && ...' now succeeds under the real generated profile. go build/vet/test green. * fix(sandbox): skip path-ancestors for filesystem roots (invalid SBPL) Follow-up to the ancestor-metadata traversal fix. A read root of "/" has no ancestors, and (path-ancestors "/") is invalid SBPL — sandbox-exec aborts with exit 65 and the sandboxed command can't launch at all. This was reachable via the compat builder (ReadRoots=["/"] flipped to restricted under EnforceWorkspace) and any degenerate "/" workspace. seatbeltAncestorMetadataRule now skips any root that is its own parent (filepath.Dir(clean)==clean), neutralizing "/" and volume roots regardless of entry point. Verified: the compat profile now compiles under sandbox-exec (exit 0). Found by adversarial verification of the prior commit. * fix(sandbox): grant the CLT toolchain so git/clang/make work On macOS, /usr/bin/git (and clang, make, etc.) are thin stubs that resolve the real binary under the active developer dir — usually /Library/Developer/CommandLineTools. The sandbox's platform read roots didn't include it, so the stub couldn't reach the real tool and failed with the misleading 'xcode-select: No developer tools were found' error. Add /Library/Developer (read-only) to the platform read roots. * fix(sandbox): allow reading the user's global git config git reads identity/config from the user's global git config, but the sandbox confines reads away from HOME, so it failed with 'unable to access ~/.gitconfig: Operation not permitted'. Grant read-only on the git config FILES specifically (~/.gitconfig and ~/.config/git/config) — not the ~/.config/git directory, which can hold an XDG credential store — so git runs while credentials and the rest of HOME stay unreadable. The path-ancestors traversal grant covers reaching them. * fix(sandbox): let the agent terminate user-owned processes on request The seatbelt profile only allowed signaling the agent's own process group ((target self) (target pgrp)), so a process started by a PREVIOUS session — e.g. a dev server still listening on a port — was in a different group and could not be killed. The agent would thrash through kill/pkill/osascript/sudo, all denied, and never stop the process the user asked it to terminate. Relax to (allow signal). The kernel still enforces UID ownership: a sandboxed command runs as the user, so it can only signal the user's own processes, never root's or another user's. Verified end-to-end: a sandboxed command now kills a different-group user-owned process under the real generated profile. * fix(sandbox): let the agent find processes + guide it to working tools Pairs with the signal fix (8b5018d). Two more gaps blocked process management: - process-info was scoped to (target same-sandbox), so the agent couldn't even inspect processes outside its own sandbox. Relaxed to (allow process-info*). - `ps` is setuid root and macOS sandbox-exec refuses to exec setuid binaries (privilege-escalation guard) — unfixable via profile. `pgrep` needs a blocked system service. So the agent kept thrashing on ps/pgrep/os.kill. Add macOS shell guidance pointing the model at the tools that DO work: lsof to find a process, kill to stop it. With the signal grant, lsof+kill now manages processes reliably (verified end-to-end). * fix(sandbox): grant git config at the seatbelt rule, not the shared profile The git-config read paths (~/.gitconfig, ~/.config/git/config) were added to the cross-platform permissionProfileReadRoots, so they leaked HOME-dependent absolute paths into the platform-agnostic 'sandbox policy --json' snapshot and broke the golden test on every OS (machine-specific paths + Windows separators). Move the grant to seatbeltReadRule (macOS only, where reads are actually confined): the PermissionProfile stays platform-agnostic so the golden is stable, and the seatbelt profile still grants the config files + their ancestor metadata for traversal. Verified: golden test green, full 'go test ./...' green, and git still reads ~/.gitconfig under the real generated profile. * fix(sandbox): grant ancestor metadata for platform read roots too Addresses CodeRabbit review on #302. seatbeltAncestorMetadataRule was passed only fs.ReadRoots + gitConfig, on the assumption that platform read roots are top-level or already covered. /Library/Developer (the CLT toolchain) breaks that: it needs /Library stat-able as an ancestor, so a chdir-style traversal into it (cd /Library/Developer/CommandLineTools) failed with ENOTDIR even though reads succeeded (exec doesn't require ancestor-stat the way chdir does). Include the platform read roots in ancestorRoots when IncludePlatformRoots is set. Verified: cd into the CLT toolchain now succeeds under the real profile; golden + sandbox tests green. Regression test added. |
||
|
|
6fb0231df8 |
TUI scrollback re-architecture, Lime chat surface, and deep-audit fixes (#158)
* docs(design): add Lime TUI prototype and rebuild spec
* tui: remove the zeroline skin system
One design remains in the tree ahead of the Lime rebuild: the zeroline
package, its TUI view/skin plumbing (Options.Skin/ThemeVariant/ThemeDark,
model skin branches, theme picker), and the zeroline CLI subcommand are
gone. /theme keeps its shell-only message.
* tui: replace the cyan palette with the Lime token table
theme.go is rebuilt around the Lime design tokens (docs/design/
zero_tui_lime.html): near-black panels, one lime accent, solid tint
stand-ins for the prototype's rgba overlays. Existing render sites map
onto the new role styles (ink/line/userPrompt/toolName/toolArg) without
layout changes; no hex exists outside theme.go.
* tui: build the Lime chat-surface components
Title bar with brand badge + context window, empty state replacing the
deleted splash (block-art 0, tagline, 1-3 starter suggestions), stream
blocks (user prompt gutter, muted interim with cursor, final-answer
accent rail, done line, bordered notes), tool cards with diff/read/
bash/grep bodies and MiniDot run spinner, borderless composer with
run-state hints, and the grouped status line. Final answers are marked
at append time; resolved tool calls collapse into their result cards.
* tui: fix Stage 2 review findings
Adversarial review of the chat-surface commit confirmed 15 issues, all
fixed here:
- wrapPlainText sliced wide runes by rune count: CJK/emoji prose could
panic the TUI or emit lines ~2x the measure; split at width instead
- a cancelled run's streamingText leaked into the next turn's interim
block; cancelRun now clears it
- orphaned tool-call cards (cancelled/errored turns) re-animated with
the spinner on any later run; the glyph is now scoped to the call's
own runID
- starter-suggestion digit capture was active while the chips were off
screen (/clear mid-run); the guard now mirrors the render condition
- prompt submits while a deferred Ctrl+C quit is draining could start a
run the quit would orphan; submission is blocked while exiting
- the running placeholder advertised ctrl+c (which quits) as the
interrupt key; it now says esc
- read-card parser expected 'N: text' but read_file emits 'N | text',
so the gutter and L-range never rendered on real output
- toolCard borders were one column short of the body rows
- diff tint bands now span the full row; NEW FILE renders green on
addBg; preamble/no-newline lines are unnumbered meta and no longer
shift hunk numbering
- card bodies and system notes paint the panel surface; the head gains
the separate faintest arg column (grep target + pattern)
- title-bar overflow now truncates the minimal candidate instead of
rendering the deleted design's 'ZERO … READY' line
- untracked the stray snake-game.html (left on disk, ignored)
* tui: restyle the interactive surfaces for Lime
Permission prompts render as the amber permission card (PERMISSION
badge, risk readout, key chips); resolved prompts collapse to one line
(allowed once/always/denied · tool) and unprompted allows fold into the
tool card's [auto] tag. Ask-user and spec-review prompts share the card
language with line borders. Autocomplete and picker overlays move onto
the panel/selection tints with the accent ❯ marker; model-picker rows
gain the provider dot and ctx · KEY_ENV metadata from the registry and
provider catalog. /resume renders its list as stacked session cards
(id + age, title, meta). Key handling is untouched everywhere.
* tui: adaptive width tiers, doc updates, final polish
Width tiers re-evaluate from the live width: ≥100 renders the full
spec; 80–99 drops the tool-arg column, header ctx, and the status
'interactive' group; 58–79 drops the diff/read gutters, bares the
badge, and keeps provider+tokens+mode; <58 renders a single-segment
header, rail-less cards, and a mode-only status line. Table-driven
tests cover {58,70,80,100,120} plus a frame-wide invariant that no
emitted line exceeds the terminal at any tier. Card bodies expand tabs
so width math holds. Dead splash-chip machinery (startupCommandNames,
startupOrder) is gone; README's TUI section now describes Lime.
* tui: fix final-review findings across Stages 3-4
A second adversarial review confirmed 14 issues, all addressed:
- permission collapse and tool-card pairing are now run-scoped (rcKey):
providers with repeating synthetic ToolCallIDs (Gemini gemini_tool_N)
could attribute a decision or result to a different run's call
- rehydrated always-grants no longer mislabel as 'allowed once'
(GrantMatched fallback)
- the width invariant now actually holds everywhere: empty-state
tagline/hint, ask-user rows, permission detail blobs, pending image
chips, and the sessions trailing hint are wrapped or fitted; the
invariant test renders the empty state and all of those rows at
widths 24-120
- every card (permission, ask-user, spec-review, sessions, notes)
drops side borders on tiny terminals, not just tool cards
- /resume card fields are sanitized against separator bytes from
user-controlled titles
- the picker's selected row paints its gap so the selBg band is solid
- dead splash helpers (normalizedStartupWidth, countLines, nonEmpty,
unreachable rowWelcome branch) and stale /theme comments are gone
- README no longer claims transcript scrolling keys or Tab-accept
behavior that never existed; chatWidth documents its 24-col floor
Tag tui-lime-s4 moves to this commit.
* audit: deep-audit report, TUI scrollback re-architecture, and cross-module fixes
TUI: settled-row flush frontier prints history to native terminal
scrollback (tea.Println, ordered + ack-serialized); View() renders only
the live tail. Full diffs (400-line flush cap) and OSC 8 file:// links
land in history; streamed interim text, cancellation markers, ask_user
exchanges, and composer input history are preserved; run-scoped dedup
keys fix Gemini's repeating tool-call ids; /exit, Ctrl+C-after-Esc, and
cross-session late flushes no longer orphan or contaminate checkpoints.
Cross-module: zero search panic + offset mapping, usage report
robustness, 529 retry, SSE keep-alive watchdog, registry corrections,
update --check on dev builds, MCP ping + mcp list --json secret
redaction, sandbox.network config knob, doctor apiKey presence, atomic
config writes, C1-safe notifications, rune-safe truncation everywhere,
gofmt + vet CI gates, dead glamour dependency dropped.
Full audit report: docs/audit/2026-06-10-deep-audit.md (175 findings).
Co-Authored-By: Claude Fable 5 <[email protected]>
* tui: hide malformed ask_user tool errors
* fix tui malformed tool retries
* fix max-turn finalization
* upgrade tui agent guardrails
* fix coderabbit guardrail findings
---------
Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: Vasanthdev2004 <[email protected]>
|