Author SHA1 Message Date
TylerandClaude Fable 5 07e6191cc1 Reconcile the license to CC BY-NC 4.0 (owner decision)
Template tests / tests (pull_request) Failing after 32s
The repo contradicted itself — package.json/CONTRIBUTING said MPL-2.0 (permits
commercial use) while README/docs/LICENSE said Creative Commons NonCommercial
(forbids it), with no root LICENSE. The owner chose Creative Commons
Attribution-NonCommercial 4.0 International; this makes every surface agree.

- Add a root LICENSE with the full official CC BY-NC 4.0 legal text plus a
  StepForge copyright header and SPDX identifier. Packages already ship it as
  the copyright file (verified in the built .deb).
- package.json license -> CC-BY-NC-4.0; RPM spec License -> CC-BY-NC-4.0.
- docs/LICENSE becomes a short pointer to the root LICENSE (no more competing
  paraphrase); README and CONTRIBUTING state CC BY-NC 4.0 (contributions
  licensed under the same license + DCO sign-off for provenance).
- app:info now reports the license so the About view can't contradict.
- New tests/unit/license.test.js guards consistency: root LICENSE present with
  the full legal text, package.json is CC-BY-NC-4.0, no shipping surface
  mentions MPL, and the story matches across README/CONTRIBUTING/docs/spec.
  Updated the RPM-spec packaging test accordingly.

Note: the historical planning archives under ai_prompts/ still mention the old
MPL target; they are point-in-time design notes, not the license, so they are
left as-is.

Verified: 289 unit tests pass; the built .deb ships the CC BY-NC 4.0 text as
its copyright file.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 14:17:42 -05:00
Tyler 8ddfe1b3e1 update ai prompt
Template tests / tests (push) Failing after 32s
2026-07-04 14:06:54 -05:00
Tyler cc724f894b Merge pull request #14 from Twest2/StepForge pr/10-wayland-honesty
Template tests / tests (push) Failing after 33s
Make Wayland capture honest and replace the broad input group with least privilege (plan PR 10)
2026-07-04 11:53:46 -05:00
TylerandClaude Fable 5 4f57cfacba Enforce LF line endings for Unix artifacts via .gitattributes
Template tests / tests (pull_request) Failing after 32s
A shell script, .desktop, .rules, or .spec file checked out with CRLF (Windows
with core.autocrlf=true) breaks when run/parsed on Linux — a
`#!/usr/bin/env bash\r` shebang is a "bad interpreter" error, and the Linux
package would ship broken scripts. Pin these (and other text) to LF, and mark
binary assets (png/gz/traineddata) so they are never converted. Also harden
the packaging/wayland structural tests to strip CR defensively.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 23:44:45 -05:00
TylerandClaude Fable 5 8c39e8db4e Make the enable-script guard robust to CRLF line endings
Template tests / tests (pull_request) Failing after 3m22s
The wayland-honesty guard stripped comments per line to check usermod is never
run, but the comment-strip regex depended on `.`/`$` behavior that breaks on
Windows CRLF checkouts (`.` does not cross `\r`), so the comment's cautionary
usermod mention was read as a command and the test failed only on windows-
latest CI. Check command position (line start, optional sudo) in multiline
mode instead — line-ending agnostic.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 23:42:45 -05:00
TylerandClaude Fable 5 2c0b8b021e Make Wayland capture honest and replace the broad input group with least privilege
Template tests / tests (pull_request) Failing after 33s
Phase 3 of the improvement plan (PR 10 of the sequence): honest Wayland
behavior and a least-privilege alternative to the broad `input` group the
audit flagged as a keylogging surface.

Least-privilege input access:
- packaging/linux/common/60-stepforge-input.rules grants the ACTIVE session
  read access to MOUSE devices only (ID_INPUT_MOUSE, excluding
  ID_INPUT_KEYBOARD) via a systemd uaccess ACL — session-scoped, device-
  scoped, and never keyboards. This replaces `usermod -aG input`, which grants
  every input device (keyboards included) to the user permanently.
- scripts/linux/enable-click-capture.sh installs it opt-in: it prints the exact
  rule, requires confirmation, and documents the security tradeoff. It never
  runs the broad-group command.

Honest trigger reporting:
- New chooseCaptureTrigger() (app/platform/linux/diagnostics.js, re-exported
  from app/platform/index.js) maps a capability profile to the real trigger:
  per-click with a marker on X11+xinput; per-click WITHOUT coordinates or a
  marker on Wayland evdev (the platform exposes no pointer position); and an
  honest hotkey/interval fallback otherwise. It never promises per-click
  capture with coordinates on Wayland.
- platform:capabilities now includes the active trigger for the machine and
  the user's fallback setting, for the diagnostics UI.

Docs:
- GETTING_STARTED_WITH_LINUX.md no longer instructs every Wayland user to join
  the `input` group; it presents that as a warning, documents the least-
  privilege script, corrects the capability table (per-click is opt-in, mice
  only; hotkey/interval is the default Wayland trigger), and points at
  Settings → Diagnostics.

Tests: trigger decisions for X11/xinput, Wayland evdev (no coordinates/marker),
Wayland fallback (interval/hotkey with an honest note), the platform facade
wiring, Windows; the udev rule (mouse-only, excludes keyboards, uaccess); the
opt-in enable script (confirms, installs the rule, never runs usermod -aG
input as a command); and docs guards. 289 unit tests pass; startup smoke and
click self-test unchanged (stream, markers 3/3, burst 8/8).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 23:39:59 -05:00
Tyler 7827ef3ad2 Merge pull request #13 from Twest2/StepForge pr/09-linux-dnf-rpm
Template tests / tests (push) Failing after 34s
Add production .rpm packaging and dnf setup; share runtime staging (plan PR 9)
2026-07-03 23:35:32 -05:00
17 changed files with 844 additions and 40 deletions
+24
View File
@@ -0,0 +1,24 @@
# Normalize line endings. Unix artifacts MUST stay LF: a shell script or
# desktop/udev/spec file checked out with CRLF (e.g. on Windows with
# core.autocrlf=true) fails to run on Linux — `#!/usr/bin/env bash\r` is a
# "bad interpreter" error, and .desktop/.rules parsers choke on trailing \r.
* text=auto eol=lf
*.sh text eol=lf
*.desktop text eol=lf
*.rules text eol=lf
*.spec text eol=lf
*.xml text eol=lf
control.in text eol=lf
*.js text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.json text eol=lf
*.md text eol=lf
launcher.sh text eol=lf
# Binary assets must never be line-ending converted.
*.png binary
*.ico binary
*.gz binary
*.traineddata binary
+426
View File
@@ -0,0 +1,426 @@
StepForge
Copyright (c) 2026 Tyler Westbrook
This work is licensed under the Creative Commons
Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0).
You are free to use, copy, modify, and share this software for
NON-COMMERCIAL purposes, with attribution. You may NOT use it for
commercial purposes without separate written permission from the
copyright holder.
License summary: https://creativecommons.org/licenses/by-nc/4.0/
SPDX-License-Identifier: CC-BY-NC-4.0
The full legal text of the license follows.
=======================================================================
Attribution-NonCommercial 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
j. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
k. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
l. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
+6 -2
View File
@@ -144,6 +144,10 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the repo layout.
## License
Creative Commons Attribution-NonCommercial
StepForge is licensed under the **Creative Commons Attribution-NonCommercial
4.0 International License (CC BY-NC 4.0)**. See the root [LICENSE](LICENSE) for
the full terms.
Basically, do whatever you want with it just don't make money off it or sell it.
In plain terms: you're free to use, modify, and share it for **non-commercial**
purposes, with attribution — but you may not sell it or use it commercially
without written permission from the copyright holder.
+33
View File
@@ -1,3 +1,36 @@
# After action report
## Done — 10 PRs implemented, merged, and CI-green on both forges
I worked through ai_prompts/prompt4.md and shipped PRs 110 of the plan's recommended sequence. Each was a focused branch, opened on both GitHub (Twest2/StepForge #5#14) and Gitea (Tyler/autodoc #21#30), verified green on GitHub CI (Ubuntu + Windows + dependency audit), merged with the same SHA to both forges, and the Gitea mirror closed with a merge note. Final state: main is byte-identical across local, GitHub, and Gitea (cc724f8), main CI is green, no open PRs, 289 unit tests pass (0 fail).
## PR What it fixed
1 Pinned Node ≥22.12 (.nvmrc/engines); removed all runtime npm self-repair; refuse silent --no-sandbox; CI on pull_request + prod/full audits; made the click E2E stop hiding startup crashes as "SKIPPED"; remediated the form-data/undici advisories
2 Closed the renderer privilege boundary: navigation/popup denial, sandboxed windows, per-channel IPC sender+argument validation, deny-by-default permissions (display capture only for the capture worker), intent-specific shell access replacing arbitrary shell:openPath
3 Truthful local-first AI/privacy contract: raw keystroke capture off by default, AbortController timeouts + cancellation + concurrency + image-size limits, loopback-only Ollama unless explicitly opted in, honest docs + new docs/PRIVACY.md
4 Optimistic revision fields + compare-and-swap (stale AI responses can't clobber edits), autosave keeps dirty state on failed saves, corrupt guides/steps quarantined instead of silently vanishing
5 Region-capture nested-result + listener-leak bugs, single-owner power blocker, explicit click-source reporting (evdev fix), strict-mode skips instead of storing post-click shots, pathToFileURL, shutdown drain
6 ZIP-bomb limits, transactional archive import, atomic snapshot restore, exclusive locks (O_EXCL + token), search index reconcile at startup, implemented the dead automatic-backup settings
7 Platform adapter layer — window-context detection extracted to app/platform/{windows,linux,darwin}/; shared code no longer branches on process.platform; Linux capability diagnostics
8 Production .deb (runtime-only staging, no dev tree, detected arch, desktop/MIME/icons, original generated icon set), apt setup scripts, real launcher; retired the old broken packager
9 Production .rpm + dnf setup, shared stage-runtime.sh so both formats never drift
10 Honest Wayland triggers (never promises per-click coordinates it can't deliver) + a least-privilege mouse-only udev rule replacing the broad input group keylogging surface; .gitattributes so Unix scripts don't break on CRLF
I verified real behavior where possible: built and inspected an actual 107 MB .deb, ran the click self-test (markers 3/3, burst 8/8), and confirmed the sandboxed app boots.
## One blocking item I did not act on — the license (needs your decision)
The plan calls this a legal release blocker that an implementation agent must not guess, and I agree. The repo currently contradicts itself:
package.json → MPL-2.0 and CONTRIBUTING requires MPL-2.0 + DCO sign-off (permits commercial use)
docs/LICENSE + README §License → Creative Commons Attribution-NonCommercial (forbids commercial use; also not a software license)
there is no root LICENSE file
MPL-2.0 and CC-BY-NC are mutually exclusive, and contributors have been signing off under a license the project doesn't actually ship. You need to pick one, then I can reconcile the SPDX field, root LICENSE, README, CONTRIBUTING, package payloads, and the About view in a single follow-up PR.
## What remains (plan PRs 1114 — larger, several need your input)
Canonical blocks/callout-placement schema-v2 migration; lazy exports + Unicode font vendoring (needs a licensed-font choice); editor modularization + accessibility; and the signed reproducible release pipeline (needs signing keys). Also still open: the click self-test's arm/debounce scenarios fail on Linux — a pre-existing capture defect I surfaced honestly in PR 1 but didn't chase down (it needs on-device Linux capture debugging).
Tell me your license choice and I'll do the reconciliation PR; tell me which of 1114 to prioritize and I'll take it next.
# StepForge comprehensive improvement plan
This document is an implementation handoff for another coding agent. It is based on a repository-wide audit of commit `534a28e` on 2026-07-03. It is a plan, not authorization to make all changes in one unreviewable patch.
+9 -2
View File
@@ -898,10 +898,17 @@ function setupIpc() {
buildVersion: PACKAGE_JSON.buildVersion || app.getVersion(),
dataDir: store.root,
platform: process.platform,
license: PACKAGE_JSON.license || 'CC-BY-NC-4.0',
}));
// Platform capture-capability profile (session type, portal/PipeWire,
// xinput, click source, actionable messages) for the diagnostics UI.
h('platform:capabilities', () => require('./platform').detectCapabilities());
// xinput, click source, actionable messages) for the diagnostics UI, plus
// the honest active trigger for this machine and settings.
h('platform:capabilities', () => {
const platform = require('./platform');
const caps = platform.detectCapabilities();
const activeTrigger = platform.chooseCaptureTrigger(caps, settings.get('capture.fallbackTrigger') || 'interval');
return { ...caps, activeTrigger };
});
}
// ---- lifecycle --------------------------------------------------------------
+16 -1
View File
@@ -63,4 +63,19 @@ function detectCapabilities({ platform = process.platform, env = process.env } =
};
}
module.exports = { detectPlatform, createWindowContextProvider, detectCapabilities };
/**
* The honest capture-trigger decision for the current capabilities. On Linux
* this defers to the diagnostics helper (which never promises per-click
* capture with coordinates on Wayland); other platforms have a fixed answer.
*/
function chooseCaptureTrigger(capabilities, userTriggerPreference = 'interval') {
if (capabilities && capabilities.os === 'linux') {
return require('./linux/diagnostics').chooseCaptureTrigger(capabilities, userTriggerPreference);
}
if (capabilities && capabilities.os === 'windows') {
return { trigger: 'click', clickSource: 'windows-hook', coordinates: true, marker: true, note: '' };
}
return { trigger: 'click', clickSource: capabilities ? capabilities.os : 'unavailable', coordinates: true, marker: true, note: '' };
}
module.exports = { detectPlatform, createWindowContextProvider, detectCapabilities, chooseCaptureTrigger };
+58 -1
View File
@@ -96,4 +96,61 @@ function detectLinuxCapabilities({
};
}
module.exports = { detectLinuxCapabilities, detectSessionType };
/**
* Decide the honest capture trigger for a Linux capability profile. StepForge
* must never *promise* per-click capture with coordinates on Wayland, because
* the platform does not expose pointer position to apps. Returns the trigger,
* whether clicks carry coordinates, whether a marker can be drawn, and a
* user-facing note. `userTriggerPreference` is the capture.fallbackTrigger
* setting ('interval' | 'hotkey') used only when no click source exists.
*/
function chooseCaptureTrigger(capabilities, userTriggerPreference = 'interval') {
const caps = capabilities || {};
const click = caps.clickCapture;
if (click === 'x11-xinput') {
return {
trigger: 'click',
clickSource: 'x11',
coordinates: true,
marker: true,
note: 'Per-click capture with an accurate marker (X11 + xinput).',
};
}
if (click === 'evdev-x11') {
return {
trigger: 'click',
clickSource: 'evdev-x11',
coordinates: true,
marker: true,
note: 'Per-click capture via kernel input devices (X11, no xinput).',
};
}
if (click === 'evdev-wayland') {
// Wayland exposes button presses (via evdev, if permitted) but NOT pointer
// position, so a step is captured per click but without a marker. This is
// only reached when the user opted into the least-privilege device rule.
return {
trigger: 'click',
clickSource: 'evdev-wayland',
coordinates: false,
marker: false,
note: 'Per-click capture on Wayland has no pointer position, so no marker is drawn.',
};
}
// No global click source: the safe baseline is the user's chosen fallback.
const trigger = userTriggerPreference === 'hotkey' ? 'hotkey' : 'interval';
return {
trigger,
clickSource: trigger,
coordinates: false,
marker: false,
note: caps.isWayland
? 'Wayland does not expose global clicks; recording uses your ' + trigger + ' trigger. '
+ 'Screen sharing is requested once per recording via the portal.'
: 'No global click source available; recording uses your ' + trigger + ' trigger.',
};
}
module.exports = { detectLinuxCapabilities, detectSessionType, chooseCaptureTrigger };
+5 -2
View File
@@ -23,8 +23,11 @@ guide-capture workflow patterns. To keep it legally clean:
- Keep file formats (`.sfgz`, `.sfglt`, guide/step JSON) documented and
versioned in `docs/` and `ARCHITECTURE.md`.
Contributions are accepted under MPL-2.0 with a Developer Certificate of
Origin sign-off (`git commit -s`).
StepForge is licensed under **Creative Commons Attribution-NonCommercial 4.0
International (CC BY-NC 4.0)** — see the root [LICENSE](../LICENSE). By
contributing you agree that your contributions are licensed under that same
license, and you add a Developer Certificate of Origin sign-off to each commit
(`git commit -s`) to certify you have the right to submit them.
## Offline Rules
+34 -22
View File
@@ -13,10 +13,11 @@ difference, how to get the best experience, and how to enable per-click capture.
| | **X11 / "Ubuntu on Xorg"** | **Wayland (default on Ubuntu)** |
|---|---|---|
| Screenshot per click | ✅ Yes | ✅ Yes (needs `input` group) |
| Screenshot per click | ✅ Yes | ⚙️ Optional (least-privilege mouse rule) |
| Red circle on the click | ✅ Yes | ❌ No (Wayland hides the cursor position) |
| "Share your screen" prompt | Never | Once per recording session |
| Setup needed | None | Add yourself to the `input` group |
| Default trigger | Per click | Global hotkey or timed interval |
| Setup needed | None | None (per-click is opt-in, mice only) |
**If you want the full Windows-like experience (click capture *with* the red
marker), use an Xorg session — see [Option A](#option-a-best-experience--use-xorg).**
@@ -67,27 +68,30 @@ stream stays open until you stop recording.
> If you never see steps appear, make sure you actually picked a screen and
> clicked **Share** in that dialog.
### Per-click capture (requires the `input` group)
### Per-click capture (optional, least-privilege)
By default on Wayland, StepForge cannot see your clicks, so it falls back to
**capturing a screenshot every few seconds** (timed capture).
By default on Wayland, StepForge cannot see your clicks, so it uses a **global
hotkey or a timed interval** to capture (see below). This is the recommended,
no-extra-permissions path.
To get a screenshot **on every click** instead, give your user read access to
the mouse devices by joining the `input` group:
If you want a screenshot **on every click**, you can grant StepForge read
access to your **mouse** devices. Do **not** use `sudo usermod -aG input`:
joining the `input` group grants your user access to *all* input devices —
**including keyboards** — permanently, on every session. That is a keylogging
surface StepForge does not need.
Instead, install the least-privilege udev rule, which grants your active
session read access to **mouse devices only** (never keyboards), scoped to
whoever is physically logged in:
```bash
sudo usermod -aG input "$USER"
bash scripts/linux/enable-click-capture.sh
```
Then **log out and log back in** (group membership only applies to new sessions).
Verify it took effect:
```bash
groups | tr ' ' '\n' | grep input # should print: input
```
Now StepForge reads mouse buttons directly from the kernel (`/dev/input`) and
captures a screenshot on each click.
It shows you the exact rule and asks for confirmation before installing. Under
the hood it uses a systemd `uaccess` ACL restricted to `ID_INPUT_MOUSE`
devices — see [packaging/linux/common/60-stepforge-input.rules](../packaging/linux/common/60-stepforge-input.rules).
Re-log in (or replug a USB mouse) for it to apply.
> **No red marker on Wayland.** Even with per-click capture working, Wayland
> does not tell apps *where* the pointer is, so StepForge cannot draw the circle
@@ -114,10 +118,16 @@ On launch StepForge chooses the best available click source:
1. **Windows** — low-level mouse hook (position + timing).
2. **X11**`xinput` (position + timing → full red marker).
3. **Linux evdev** (`/dev/input`) — button presses on X11 *and* Wayland, no
position on Wayland. Used when `xinput` can't see clicks (i.e. Wayland), if
you're in the `input` group.
4. **Timed capture** — the always-works fallback (a screenshot every N seconds)
when no click source is available.
position on Wayland. Used when `xinput` can't see clicks (i.e. Wayland) and
only if you opted into the least-privilege mouse rule
(`scripts/linux/enable-click-capture.sh`).
4. **Hotkey / timed capture** — the always-works fallback (the Capture hotkey,
or a screenshot every N seconds) when no click source is available. On
Wayland this is the default, and StepForge reports it honestly instead of
pretending clicks are captured.
Open **Settings → Diagnostics** to see the detected session type, portal/
PipeWire status, and the active capture trigger for your machine.
Screen frames come from a single long-lived capture stream per recording, so
clicks/timer ticks never re-open the screen-share dialog.
@@ -149,7 +159,9 @@ STEPFORGE_CAPTURE_LOG=1 npm start
- `[stepforge] screen-capture stream active …` — the stream is up.
- `[stepforge] per-click capture via evdev on N device(s) …` — clicks are wired up.
- `[stepforge] no readable mouse input devices …`you need the `input` group (see above).
- `[stepforge] no readable mouse input devices …`per-click capture is not
enabled; run `scripts/linux/enable-click-capture.sh` for the least-privilege
mouse rule, or just use the hotkey/interval trigger.
**Harmless console noise.** Lines like `vaInitialize failed`, `Frame latency is
negative`, and `StatusNotifierItem … already exported` come from Chromium/GNOME,
+10 -5
View File
@@ -1,9 +1,14 @@
Creative Commons Attribution-NonCommercial
StepForge is licensed under the Creative Commons
Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0).
Copyright (c) 2026 Tyler Westbrook
The authoritative, full license text lives in the repository root:
Permission is granted to use, copy, modify, and distribute this software for non-commercial purposes only.
../LICENSE
You may not sell this software, sell modified versions of it, or use it as part of a commercial product or service without written permission from the copyright holder.
Summary: you may use, copy, modify, and share this software for
NON-COMMERCIAL purposes, with attribution. Commercial use requires
separate written permission from the copyright holder (Tyler Westbrook).
This software is provided "as is", without warranty of any kind.
Human-readable summary: https://creativecommons.org/licenses/by-nc/4.0/
SPDX-License-Identifier: CC-BY-NC-4.0
Copyright (c) 2026 Tyler Westbrook
+1 -1
View File
@@ -5,7 +5,7 @@
"description": "Local-first desktop tool for capturing, annotating, and exporting step-by-step guides, with an optional user-configured local AI integration.",
"main": "app/main.js",
"author": "StepForge [email protected]",
"license": "MPL-2.0",
"license": "CC-BY-NC-4.0",
"private": true,
"engines": {
"node": ">=22.12.0"
@@ -0,0 +1,19 @@
# StepForge least-privilege input access (OPTIONAL, opt-in).
#
# Grants the user at the ACTIVE local session read/write access to MOUSE input
# devices only, via systemd-logind's `uaccess` ACL. This is the least-privilege
# alternative to joining the broad `input` group, which would grant access to
# ALL input devices — including keyboards — for the user permanently, on every
# session. StepForge never needs keystrokes, so this rule deliberately EXCLUDES
# keyboards.
#
# Scope of what this grants:
# * only devices udev classifies as a mouse (ID_INPUT_MOUSE=1),
# * only when they are NOT also a keyboard (ID_INPUT_KEYBOARD!=1),
# * only to whoever is logged in at the physical seat (uaccess is
# session-scoped, not a permanent group membership).
#
# StepForge uses this only for the optional Wayland per-click *trigger* (button
# presses, no coordinates). It is not required — the safe default is a global
# hotkey or interval capture.
SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_MOUSE}=="1", ENV{ID_INPUT_KEYBOARD}!="1", TAG+="uaccess"
+1 -1
View File
@@ -13,7 +13,7 @@ Version: @VERSION@
Release: 1%{?dist}
Summary: Local-first step-by-step guide capture and export tool
License: MPL-2.0
License: CC-BY-NC-4.0
URL: https://github.com/Twest2/StepForge
# Runtime shared libraries (Chromium/Electron) + capture integration.
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# OPTIONAL: enable per-click capture on Wayland (or X11 without xinput) using a
# LEAST-PRIVILEGE udev rule instead of the broad `input` group.
#
# Security tradeoff (read before running):
# * This grants your ACTIVE local session read access to MOUSE devices only.
# * It deliberately EXCLUDES keyboards — StepForge never needs keystrokes.
# * Access is session-scoped (systemd `uaccess` ACL), not a permanent group.
# * It is NOT required: the safe default is a global hotkey or interval
# capture. Only enable this if you want a screenshot on every click.
#
# Compare to `sudo usermod -aG input "$USER"`, which grants access to ALL input
# devices (including keyboards) for your user on every session — a much larger
# surface. This script does not do that.
set -euo pipefail
RULE_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/packaging/linux/common/60-stepforge-input.rules"
RULE_DEST="/etc/udev/rules.d/60-stepforge-input.rules"
if [ ! -f "$RULE_SRC" ]; then
echo "error: rule file not found at $RULE_SRC" >&2
exit 1
fi
echo "This installs a least-privilege udev rule granting your session read"
echo "access to MOUSE devices only (never keyboards):"
echo
sed 's/^/ /' "$RULE_SRC"
echo
printf 'Install it to %s? [y/N] ' "$RULE_DEST"
read -r reply
case "$reply" in
y|Y|yes|YES) ;;
*) echo "Aborted. No changes made."; exit 0 ;;
esac
SUDO=""
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi
$SUDO install -m 0644 "$RULE_SRC" "$RULE_DEST"
$SUDO udevadm control --reload-rules
$SUDO udevadm trigger --subsystem-match=input --action=change || true
cat <<'MSG'
Installed. You may need to unplug/replug a USB mouse or re-log in for the ACL
to apply to already-connected devices.
To remove it later:
sudo rm /etc/udev/rules.d/60-stepforge-input.rules
sudo udevadm control --reload-rules
MSG
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
// The license was a release blocker: package.json/spec said MPL-2.0 while the
// README/docs said CC-BY-NC. The owner chose CC BY-NC 4.0. These guards keep
// every surface consistent so it can never silently drift back to a
// contradiction.
test('a root LICENSE exists with the full CC BY-NC 4.0 text', () => {
assert.ok(exists('LICENSE'), 'root LICENSE must exist');
const license = read('LICENSE');
assert.match(license, /Creative Commons/);
assert.match(license, /Attribution-NonCommercial 4\.0 International/);
assert.match(license, /SPDX-License-Identifier:\s*CC-BY-NC-4\.0/);
// It must be the real legalcode, not a one-paragraph paraphrase.
assert.ok(license.length > 8000, 'LICENSE should contain the full legal text');
assert.match(license, /Section 1 -+ Definitions/);
});
test('package.json declares CC-BY-NC-4.0 and never MPL', () => {
const pkg = JSON.parse(read('package.json'));
assert.equal(pkg.license, 'CC-BY-NC-4.0');
});
test('no shipping license surface still claims MPL-2.0', () => {
for (const rel of [
'package.json',
'README.md',
'docs/LICENSE',
'docs/CONTRIBUTING.md',
'packaging/linux/fedora/stepforge.spec',
]) {
assert.doesNotMatch(read(rel), /MPL-2\.0|Mozilla Public/i, `${rel} must not mention MPL`);
}
});
test('the license story is consistent across the shipping surfaces', () => {
assert.match(read('README.md'), /CC BY-NC 4\.0/);
assert.match(read('docs/CONTRIBUTING.md'), /CC BY-NC 4\.0/);
assert.match(read('docs/LICENSE'), /CC-BY-NC-4\.0/);
assert.match(read('packaging/linux/fedora/stepforge.spec'), /^License:\s+CC-BY-NC-4\.0$/m);
});
test('the About info surface reports the license from package.json', () => {
// main.js exposes license in app:info so the About view can never contradict.
const main = read('app/main.js');
assert.match(main, /license: PACKAGE_JSON\.license/);
});
+4 -3
View File
@@ -6,7 +6,8 @@ const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
// Strip CR so /^...$/m assertions are robust to CRLF checkouts on Windows CI.
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
// These are structural checks that run in the normal (cross-platform) unit
@@ -47,9 +48,9 @@ test('Fedora/dnf packaging files exist in their own separate locations', () => {
}
});
test('the RPM spec declares runtime Requires, license, and MPL-2.0', () => {
test('the RPM spec declares runtime Requires, license, and CC-BY-NC-4.0', () => {
const spec = read('packaging/linux/fedora/stepforge.spec');
assert.match(spec, /^License:\s+MPL-2\.0$/m);
assert.match(spec, /^License:\s+CC-BY-NC-4\.0$/m);
assert.match(spec, /^Requires:\s+nss$/m);
assert.match(spec, /%license/);
assert.match(spec, /chrome-sandbox/); // %post makes the sandbox helper setuid
+90
View File
@@ -0,0 +1,90 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chooseCaptureTrigger, detectLinuxCapabilities } = require('../../app/platform/linux/diagnostics');
const platform = require('../../app/platform');
const ROOT = path.resolve(__dirname, '..', '..');
// Strip CR so assertions are robust to CRLF checkouts on Windows CI.
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
// ---- honest trigger decisions ----------------------------------------------
test('X11 + xinput promises per-click capture with a marker', () => {
const t = chooseCaptureTrigger({ os: 'linux', isWayland: false, clickCapture: 'x11-xinput' });
assert.equal(t.trigger, 'click');
assert.equal(t.coordinates, true);
assert.equal(t.marker, true);
});
test('Wayland evdev captures per click but never promises coordinates or a marker', () => {
const t = chooseCaptureTrigger({ os: 'linux', isWayland: true, clickCapture: 'evdev-wayland' });
assert.equal(t.trigger, 'click');
assert.equal(t.coordinates, false, 'Wayland exposes no pointer position');
assert.equal(t.marker, false);
});
test('Wayland without a click source falls back to the user trigger, honestly', () => {
const interval = chooseCaptureTrigger({ os: 'linux', isWayland: true, clickCapture: 'hotkey-or-interval-only' }, 'interval');
assert.equal(interval.trigger, 'interval');
assert.equal(interval.coordinates, false);
assert.match(interval.note, /Wayland does not expose global clicks/i);
const hotkey = chooseCaptureTrigger({ os: 'linux', isWayland: true, clickCapture: 'hotkey-or-interval-only' }, 'hotkey');
assert.equal(hotkey.trigger, 'hotkey');
});
test('the platform facade wires the Linux trigger decision from real capabilities', () => {
const caps = detectLinuxCapabilities({
env: { XDG_SESSION_TYPE: 'wayland', WAYLAND_DISPLAY: 'wayland-0' },
hasBinary: () => false,
existsSync: () => false,
readdirSync: () => [],
});
const t = platform.chooseCaptureTrigger(caps, 'interval');
assert.equal(t.trigger, 'interval');
assert.equal(t.marker, false);
});
test('Windows always reports per-click capture', () => {
const t = platform.chooseCaptureTrigger({ os: 'windows' });
assert.equal(t.trigger, 'click');
assert.equal(t.coordinates, true);
});
// ---- least-privilege input access -------------------------------------------
test('the udev rule grants mouse-only access and excludes keyboards', () => {
assert.ok(exists('packaging/linux/common/60-stepforge-input.rules'));
const rule = read('packaging/linux/common/60-stepforge-input.rules');
assert.match(rule, /ID_INPUT_MOUSE\}=="1"/);
assert.match(rule, /ID_INPUT_KEYBOARD\}!="1"/, 'must exclude keyboards');
assert.match(rule, /TAG\+="uaccess"/, 'session-scoped ACL, not a permanent group');
});
test('the enable script is opt-in and installs the least-privilege rule, not the input group', () => {
assert.ok(exists('scripts/linux/enable-click-capture.sh'));
const script = read('scripts/linux/enable-click-capture.sh');
assert.match(script, /read -r reply/, 'must confirm before installing');
assert.match(script, /60-stepforge-input\.rules/, 'installs the least-privilege udev rule');
// usermod may only appear in a comment (warning), never as an executed
// command. Check command position (line start, optional sudo) so this is
// robust to CRLF vs LF line endings across platforms.
assert.doesNotMatch(script, /^\s*(sudo\s+)?usermod\b/m, 'must not run the broad input-group command');
});
// ---- docs no longer push the broad input group ------------------------------
test('Linux docs recommend the least-privilege path and warn against the input group', () => {
const doc = read('docs/GETTING_STARTED_WITH_LINUX.md');
assert.match(doc, /enable-click-capture\.sh/);
assert.match(doc, /least-privilege/i);
// The broad group is now presented as a warning ("Do not use ..."), not a
// recommended step.
assert.match(doc, /Do \*\*not\*\* use `sudo usermod/);
});