opencode-skills-antigravity 0.0.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +141 -22
  2. package/bundled-skills/007/scripts/full_audit.py +6 -4
  3. package/bundled-skills/007/scripts/score_calculator.py +67 -7
  4. package/bundled-skills/algorithmic-art/templates/viewer.html +2 -2
  5. package/bundled-skills/apify-actorization/SKILL.md +1 -2
  6. package/bundled-skills/apify-actorization/references/cli-actorization.md +4 -4
  7. package/bundled-skills/docs/COMMUNITY_GUIDELINES.md +1 -1
  8. package/bundled-skills/docs/contributors/community-guidelines.md +3 -32
  9. package/bundled-skills/docs/integrations/jetski-gemini-loader/loader.ts +21 -3
  10. package/bundled-skills/docs/maintainers/security-findings-triage-2026-03-18-addendum.md +22 -0
  11. package/bundled-skills/dotnet-backend-patterns/resources/implementation-playbook.md +2 -2
  12. package/bundled-skills/instagram/scripts/auth.py +15 -6
  13. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +33 -1073
  14. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +7 -4
  15. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +15 -3
  16. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +85 -88
  17. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +260 -456
  18. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +4 -2
  19. package/bundled-skills/notebooklm/scripts/auth_manager.py +17 -3
  20. package/bundled-skills/notebooklm/scripts/browser_session.py +11 -2
  21. package/bundled-skills/radix-ui-design-system/examples/README.md +1 -1
  22. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +5 -3
  23. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/app.py +21 -13
  24. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/webhook_handler.py +11 -4
  25. package/package.json +1 -1
  26. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +0 -35
  27. /package/bundled-skills/dotnet-backend-patterns/assets/{repository-template.cs → repository-template.cs.template} +0 -0
  28. /package/bundled-skills/dotnet-backend-patterns/assets/{service-template.cs → service-template.cs.template} +0 -0
  29. /package/bundled-skills/radix-ui-design-system/templates/{component-template.tsx → component-template.tsx.template} +0 -0
package/README.md CHANGED
@@ -1,31 +1,59 @@
1
- # opencode-skills-antigravity
1
+ <div align="center">
2
2
 
3
- An [OpenCode CLI](https://opencode.ai/) plugin that bundles and manages the [Antigravity Awesome Skills](https://github.com/sickn33/antigravity-awesome-skills) repository for instant use.
3
+ <img src="./docs/logo.svg" alt="OpenCode Skills Antigravity"/>
4
4
 
5
- ## ✨ How it works
5
+ <br/>
6
+ <br/>
7
+ <br/>
6
8
 
7
- This plugin ensures you always have the latest skills without any network latency at startup.
9
+ [![npm version](https://img.shields.io/npm/v/opencode-skills-antigravity?style=for-the-badge&color=cb3837&label=npm)](https://www.npmjs.com/package/opencode-skills-antigravity)
10
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-skills-antigravity?style=for-the-badge&color=orange)](https://www.npmjs.com/package/opencode-skills-antigravity)
11
+ [![license](https://img.shields.io/github/license/FrancoStino/opencode-skills-antigravity?style=for-the-badge&color=blue)](./LICENSE)
8
12
 
9
- - **Automated Updates:** A GitHub Action checks for updates in the [Antigravity Awesome Skills](https://github.com/sickn33/antigravity-awesome-skills) repository every hour. If new skills are found, it automatically bundles them and publishes a new version of the NPM package.
10
- - **Instant Deployment:** When you start OpenCode, the plugin instantly copies the pre-bundled skills to your local machine. This process works perfectly offline and ensures zero network latency during startup.
13
+ </div>
11
14
 
12
- OpenCode automatically detects all skills and makes them available to the AI agent.
15
+ # OpenCode Skills Antigravity
13
16
 
14
- You can then invoke any skill explicitly in your prompt:
17
+ > An [OpenCode CLI](https://opencode.ai/) plugin that bundles and auto-syncs the [Antigravity Awesome Skills](https://github.com/sickn33/antigravity-awesome-skills) collection — delivered instantly, with zero network latency at startup.
15
18
 
16
- ```bash
17
- opencode run /brainstorming help me plan a feature
18
- ```
19
+ ---
20
+
21
+ ## Overview
22
+
23
+ **OpenCode Skills Antigravity** bridges the OpenCode CLI with the Antigravity Awesome Skills repository. Instead of fetching skills on every startup, this plugin ships with a pre-bundled snapshot that gets copied directly to your local machine the moment OpenCode launches.
24
+
25
+ The result: skills are always fresh (synced hourly via GitHub Actions), always available (even offline), and always instant.
26
+
27
+ ---
28
+
29
+ ## How It Works
30
+
31
+ The plugin operates in two phases:
32
+
33
+ **1. Automated upstream sync (CI)**
34
+
35
+ A GitHub Actions workflow runs every hour, checking the [Antigravity Awesome Skills](https://github.com/sickn33/antigravity-awesome-skills) repository for changes. When new or updated skills are detected, the workflow:
36
+
37
+ - Re-bundles the skill files into `bundled-skills/`
38
+ - Bumps the package version (`patch`)
39
+ - Creates a tagged GitHub Release
40
+ - Publishes the new version to npm
41
+
42
+ **2. Local deployment (startup)**
19
43
 
20
- Skills are also available as `/` commands directly in the OpenCode chat (e.g., `/brainstorming`).
44
+ When OpenCode starts, the plugin runs and copies the pre-bundled skills from the npm package to:
21
45
 
22
- Or simply describe what you want and OpenCode will pick the right skill automatically.
46
+ ```
47
+ ~/.config/opencode/skills/
48
+ ```
23
49
 
24
- ## 🚀 Installation
50
+ No network calls, no latency, no failures. If the copy somehow fails, a silent fallback attempts to fetch via `npx antigravity-awesome-skills` in the background.
25
51
 
26
- ### 1. Add the plugin to your global OpenCode config
52
+ ---
27
53
 
28
- Edit (or create) `~/.config/opencode/opencode.json`:
54
+ ## Installation
55
+
56
+ Add the plugin to your global OpenCode configuration file at `~/.config/opencode/opencode.json`:
29
57
 
30
58
  ```json
31
59
  {
@@ -35,19 +63,110 @@ Edit (or create) `~/.config/opencode/opencode.json`:
35
63
  }
36
64
  ```
37
65
 
38
- OpenCode will automatically download the npm package on next startup via Bun. No manual `npm install` required.
66
+ That's it. OpenCode will automatically download the npm package on next startup via Bun no manual `npm install` needed.
67
+
68
+ ---
69
+
70
+ ## Usage
39
71
 
40
- ## 📁 Skills location
72
+ Once installed, all bundled skills are available in three ways:
41
73
 
42
- Skills are stored at:
74
+ **Explicit invocation via CLI:**
75
+ ```bash
76
+ opencode run /brainstorming help me plan a new feature
77
+ opencode run /refactor clean up this function
78
+ ```
43
79
 
80
+ **Slash commands in the OpenCode chat:**
44
81
  ```
45
- ~/.config/opencode/skills/
82
+ /brainstorming
83
+ /refactor
84
+ /document
85
+ ```
86
+
87
+ **Natural language — OpenCode picks the right skill automatically:**
88
+ ```
89
+ "Help me brainstorm ideas for a REST API design"
90
+ "Refactor this function to be more readable"
46
91
  ```
47
92
 
48
- OpenCode scans this directory automatically at startup.
93
+ ---
94
+
95
+ ## CI/CD Pipeline
96
+
97
+ The release pipeline is fully automated and self-contained:
98
+
99
+ ```
100
+ [Hourly Cron]
101
+
102
+
103
+ Auto-Sync Skills ──── no changes ───▶ (skip)
104
+
105
+ changes detected
106
+
107
+
108
+ Bump patch version + commit + tag
109
+
110
+
111
+ Create GitHub Release
112
+
113
+
114
+ Publish to npm
115
+
116
+
117
+ Sync main → develop
118
+ ```
119
+
120
+ Manual releases (minor/major/patch) can also be triggered via the **Create Release** workflow dispatch in the Actions tab.
121
+
122
+ ---
123
+
124
+ ## Project Structure
125
+
126
+ ```
127
+ opencode-skills-antigravity/
128
+ ├── src/
129
+ │ └── index.ts # Plugin entry point — copies bundled skills on startup
130
+ ├── bundled-skills/ # Pre-bundled skills snapshot (auto-updated by CI)
131
+ ├── dist/ # Compiled TypeScript output
132
+ ├── .github/
133
+ │ └── workflows/
134
+ │ ├── sync-skills.yml # Hourly skill sync + auto-publish
135
+ │ ├── release.yml # Manual version bump + GitHub Release
136
+ │ ├── publish.yml # npm publish on new release
137
+ │ └── merge-branch.yml # Keeps develop in sync with main
138
+ ├── package.json
139
+ └── tsconfig.json
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Development
145
+
146
+ **Requirements:** Node.js ≥ 20, TypeScript ≥ 5
147
+
148
+ ```bash
149
+ # Install dependencies
150
+ npm install
151
+
152
+ # Build
153
+ npm run build
154
+
155
+ # Output is in dist/
156
+ ```
157
+
158
+ The plugin is written in TypeScript and compiled to ESNext with full type declarations. It targets ES2022 and uses ESM module resolution.
159
+
160
+ ---
161
+
162
+ ## Contributing
163
+
164
+ Issues and pull requests are welcome at [github.com/FrancoStino/opencode-skills-antigravity](https://github.com/FrancoStino/opencode-skills-antigravity/issues).
165
+
166
+ If you'd like to contribute new skills to the upstream collection, head over to [antigravity-awesome-skills](https://github.com/sickn33/antigravity-awesome-skills) — they'll be automatically picked up and bundled here within the hour.
49
167
 
168
+ ---
50
169
 
51
- ## 📄 License
170
+ ## License
52
171
 
53
172
  MIT © [Davide Ladisa](https://www.davideladisa.it/)
@@ -1081,6 +1081,7 @@ def run_audit(
1081
1081
  inj_report: dict = {"findings": [], "score": 100, "total_findings": 0}
1082
1082
  quick_report: dict = {"findings": [], "score": 100, "total_findings": 0}
1083
1083
  all_findings: list[dict] = []
1084
+ report_findings: list[dict] = []
1084
1085
 
1085
1086
  if need_scanners:
1086
1087
  logger.info("Running scanners for phases %s...", [p for p in phases_list if p >= 3])
@@ -1121,6 +1122,7 @@ def run_audit(
1121
1122
  + quick_report.get("findings", [])
1122
1123
  )
1123
1124
  all_findings = score_calculator._deduplicate_findings(raw)
1125
+ report_findings = score_calculator.redact_findings_for_report(all_findings)
1124
1126
 
1125
1127
  # ------------------------------------------------------------------
1126
1128
  # Collect source files if needed for phase 6
@@ -1142,7 +1144,7 @@ def run_audit(
1142
1144
  if 2 in phases_list:
1143
1145
  # Phase 2 benefits from phase 1 data and findings
1144
1146
  surface = phases_data.get("phase1") or _phase1_surface_mapping(target, verbose=verbose)
1145
- phases_data["phase2"] = _phase2_threat_modeling_hints(surface, all_findings)
1147
+ phases_data["phase2"] = _phase2_threat_modeling_hints(surface, report_findings)
1146
1148
 
1147
1149
  if 3 in phases_list:
1148
1150
  phases_data["phase3"] = _phase3_security_checklist(
@@ -1164,10 +1166,10 @@ def run_audit(
1164
1166
  )
1165
1167
 
1166
1168
  if 4 in phases_list:
1167
- phases_data["phase4"] = _phase4_red_team_scenarios(all_findings, auth_score)
1169
+ phases_data["phase4"] = _phase4_red_team_scenarios(report_findings, auth_score)
1168
1170
 
1169
1171
  if 5 in phases_list:
1170
- phases_data["phase5"] = _phase5_blue_team_recommendations(all_findings, auth_score)
1172
+ phases_data["phase5"] = _phase5_blue_team_recommendations(report_findings, auth_score)
1171
1173
 
1172
1174
  if 6 in phases_list:
1173
1175
  phases_data["phase6"] = _phase6_verdict(
@@ -1227,7 +1229,7 @@ def run_audit(
1227
1229
  "phases_run": phases_list,
1228
1230
  "phases": phases_data,
1229
1231
  "total_findings": len(all_findings),
1230
- "findings": all_findings,
1232
+ "findings": report_findings,
1231
1233
  "report_path": str(report_path),
1232
1234
  }
1233
1235
 
@@ -64,6 +64,17 @@ import quick_scan # noqa: E402
64
64
  # ---------------------------------------------------------------------------
65
65
  logger = setup_logging("007-score-calculator")
66
66
 
67
+ _SENSITIVE_FINDING_KEYS = {
68
+ "snippet",
69
+ "secret",
70
+ "token",
71
+ "password",
72
+ "access_token",
73
+ "app_secret",
74
+ "authorization_code",
75
+ "client_secret",
76
+ }
77
+
67
78
 
68
79
  # ---------------------------------------------------------------------------
69
80
  # Positive-signal patterns (auth, encryption, resilience, monitoring)
@@ -360,6 +371,51 @@ def _bar(score: float, width: int = 20) -> str:
360
371
  return "[" + "#" * filled + "." * (width - filled) + "]"
361
372
 
362
373
 
374
+ def _redact_report_value(value):
375
+ """Recursively redact sensitive values from report payloads."""
376
+ if isinstance(value, dict):
377
+ return {key: _redact_report_value(value[key]) for key in value}
378
+ if isinstance(value, list):
379
+ return [_redact_report_value(item) for item in value]
380
+ return value
381
+
382
+
383
+ def redact_findings_for_report(findings: list[dict]) -> list[dict]:
384
+ """Return findings safe to serialize in user-facing reports."""
385
+ redacted: list[dict] = []
386
+
387
+ for finding in findings:
388
+ safe_finding: dict = {}
389
+ finding_type = str(finding.get("type", "")).lower()
390
+
391
+ for key, value in finding.items():
392
+ key_lower = key.lower()
393
+ if key_lower in _SENSITIVE_FINDING_KEYS:
394
+ safe_finding[key] = "[redacted]"
395
+ continue
396
+ if finding_type == "secret" and key_lower in {"entropy", "match", "raw", "value"}:
397
+ safe_finding[key] = "[redacted]"
398
+ continue
399
+ safe_finding[key] = _redact_report_value(value)
400
+
401
+ redacted.append(safe_finding)
402
+
403
+ return redacted
404
+
405
+
406
+ def build_safe_scanner_summaries(scanner_summaries: dict[str, dict]) -> dict[str, dict]:
407
+ """Return scanner summaries with primitive numeric values only."""
408
+ safe_summaries: dict[str, dict] = {}
409
+
410
+ for scanner_name, summary in scanner_summaries.items():
411
+ safe_summaries[scanner_name] = {
412
+ "findings": int(summary.get("findings", 0)),
413
+ "score": float(summary.get("score", 0)),
414
+ }
415
+
416
+ return safe_summaries
417
+
418
+
363
419
  def format_text_report(
364
420
  target: str,
365
421
  domain_scores: dict[str, float],
@@ -430,6 +486,7 @@ def build_json_report(
430
486
  elapsed: float,
431
487
  ) -> dict:
432
488
  """Build a structured JSON report."""
489
+ safe_findings = redact_findings_for_report(all_findings)
433
490
  return {
434
491
  "report": "score_calculator",
435
492
  "target": target,
@@ -444,7 +501,7 @@ def build_json_report(
444
501
  "emoji": verdict["emoji"],
445
502
  },
446
503
  "scanner_summaries": scanner_summaries,
447
- "findings": all_findings,
504
+ "findings": safe_findings,
448
505
  }
449
506
 
450
507
 
@@ -564,6 +621,9 @@ def run_score(
564
621
  all_findings_raw = secrets_findings + dep_findings + inj_findings + quick_findings
565
622
  all_findings = _deduplicate_findings(all_findings_raw)
566
623
  total_findings = len(all_findings)
624
+ safe_findings = redact_findings_for_report(all_findings)
625
+ safe_total_findings = len(safe_findings)
626
+ safe_scanner_summaries = build_safe_scanner_summaries(scanner_summaries)
567
627
 
568
628
  logger.info(
569
629
  "Aggregated %d raw findings -> %d unique (deduplicated)",
@@ -613,8 +673,8 @@ def run_score(
613
673
  result=f"final_score={final_score}, verdict={verdict['label']}",
614
674
  details={
615
675
  "domain_scores": domain_scores,
616
- "total_findings": total_findings,
617
- "scanner_summaries": scanner_summaries,
676
+ "total_findings": safe_total_findings,
677
+ "scanner_summaries": safe_scanner_summaries,
618
678
  "duration_seconds": round(elapsed, 3),
619
679
  },
620
680
  )
@@ -627,9 +687,9 @@ def run_score(
627
687
  domain_scores=domain_scores,
628
688
  final_score=final_score,
629
689
  verdict=verdict,
630
- scanner_summaries=scanner_summaries,
690
+ scanner_summaries=safe_scanner_summaries,
631
691
  all_findings=all_findings,
632
- total_findings=total_findings,
692
+ total_findings=safe_total_findings,
633
693
  elapsed=elapsed,
634
694
  )
635
695
 
@@ -641,8 +701,8 @@ def run_score(
641
701
  domain_scores=domain_scores,
642
702
  final_score=final_score,
643
703
  verdict=verdict,
644
- scanner_summaries=scanner_summaries,
645
- total_findings=total_findings,
704
+ scanner_summaries=safe_scanner_summaries,
705
+ total_findings=safe_total_findings,
646
706
  elapsed=elapsed,
647
707
  ))
648
708
 
@@ -20,7 +20,7 @@
20
20
  <meta charset="UTF-8">
21
21
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
22
  <title>Generative Art Viewer</title>
23
- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
23
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js" integrity="sha512-bcfltY+lNLlNxz38yBBm/HLaUB1gTV6I0e+fahbF9pS6roIdzUytozWdnFV8ZnM6cSAG5EbmO0ag0a/fLZSG4Q==" crossorigin="anonymous"></script>
24
24
  <link rel="preconnect" href="https://fonts.googleapis.com">
25
25
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
26
26
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
@@ -596,4 +596,4 @@
596
596
  });
597
597
  </script>
598
598
  </body>
599
- </html>
599
+ </html>
@@ -45,10 +45,9 @@ Verify CLI is logged in:
45
45
  apify info # Should return your username
46
46
  ```
47
47
 
48
- If not logged in, check if `APIFY_TOKEN` environment variable is defined. If not, ask the user to generate one at https://console.apify.com/settings/integrations, then:
48
+ If not logged in, check if `APIFY_TOKEN` environment variable is defined. If not, ask the user to generate one at https://console.apify.com/settings/integrations, add it to their shell or secret manager without putting the literal token in command history, then run:
49
49
 
50
50
  ```bash
51
- export APIFY_TOKEN="your_token_here"
52
51
  apify login
53
52
  ```
54
53
 
@@ -33,12 +33,12 @@ Reference the [cli-start template Dockerfile](https://github.com/apify/actor-tem
33
33
  ```dockerfile
34
34
  FROM apify/actor-node:20
35
35
 
36
- # Install ubi for easy GitHub release installation
37
- RUN curl --silent --location \
38
- https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | sh
36
+ # Install ubi from a package source or a verified release artifact
37
+ # Example: use your base image package manager or vendor a pinned binary in the build context
38
+ # RUN apt-get update && apt-get install -y ubi
39
39
 
40
40
  # Install your CLI tool from GitHub releases (example)
41
- # RUN ubi --project your-org/your-tool --in /usr/local/bin
41
+ # RUN install -m 0755 ./vendor/your-tool /usr/local/bin/your-tool
42
42
 
43
43
  # Or install apify-cli and jq manually
44
44
  RUN npm install -g apify-cli
@@ -1,3 +1,3 @@
1
1
  # Community Guidelines
2
2
 
3
- This document moved to [`contributors/community-guidelines.md`](contributors/community-guidelines.md).
3
+ This document moved to [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md).
@@ -1,33 +1,4 @@
1
- # Code of Conduct
1
+ # Community Guidelines
2
2
 
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone.
6
-
7
- ## Our Standards
8
-
9
- Examples of behavior that contributes to creating a positive environment include:
10
-
11
- - Using welcoming and inclusive language
12
- - Being respectful of differing viewpoints and experiences
13
- - Gracefully accepting constructive criticism
14
- - Focusing on what is best for the community
15
- - Showing empathy towards other community members
16
-
17
- Examples of unacceptable behavior by participants include:
18
-
19
- - The use of sexualized language or imagery and unwelcome sexual attention or advances
20
- - Trolling, insulting/derogatory comments, and personal or political attacks
21
- - Public or private harassment
22
- - Publishing others' private information without explicit permission
23
- - Other conduct which could reasonably be considered inappropriate in a professional setting
24
-
25
- ## Enforcement
26
-
27
- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
-
29
- ## Attribution
30
-
31
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1.
32
-
33
- [homepage]: https://www.contributor-covenant.org
3
+ The canonical project policy now lives in the repository root at
4
+ [`CODE_OF_CONDUCT.md`](../../CODE_OF_CONDUCT.md).
@@ -83,16 +83,34 @@ export async function loadSkillBodies(
83
83
  ): Promise<string[]> {
84
84
  const bodies: string[] = [];
85
85
  const rootPath = path.resolve(skillsRoot);
86
+ const rootRealPath = await fs.promises.realpath(rootPath);
86
87
 
87
88
  for (const meta of metas) {
88
- const fullPath = path.resolve(rootPath, meta.path, "SKILL.md");
89
- const relativePath = path.relative(rootPath, fullPath);
89
+ const skillDirPath = path.resolve(rootPath, meta.path);
90
+ const relativePath = path.relative(rootPath, skillDirPath);
90
91
 
91
92
  if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
92
93
  throw new Error(`Skill path escapes skills root: ${meta.id}`);
93
94
  }
94
95
 
95
- const text = await fs.promises.readFile(fullPath, "utf8");
96
+ const skillDirStat = await fs.promises.lstat(skillDirPath);
97
+ if (!skillDirStat.isDirectory() || skillDirStat.isSymbolicLink()) {
98
+ throw new Error(`Skill directory must be a regular directory inside the skills root: ${meta.id}`);
99
+ }
100
+
101
+ const fullPath = path.join(skillDirPath, "SKILL.md");
102
+ const skillFileStat = await fs.promises.lstat(fullPath);
103
+ if (!skillFileStat.isFile() || skillFileStat.isSymbolicLink()) {
104
+ throw new Error(`SKILL.md must be a regular file inside the skills root: ${meta.id}`);
105
+ }
106
+
107
+ const realPath = await fs.promises.realpath(fullPath);
108
+ const realRelativePath = path.relative(rootRealPath, realPath);
109
+ if (realRelativePath.startsWith("..") || path.isAbsolute(realRelativePath)) {
110
+ throw new Error(`SKILL.md resolves outside the skills root: ${meta.id}`);
111
+ }
112
+
113
+ const text = await fs.promises.readFile(realPath, "utf8");
96
114
  bodies.push(text);
97
115
  }
98
116
 
@@ -0,0 +1,22 @@
1
+ # Security Findings Triage Addendum (2026-03-18)
2
+
3
+ This addendum supersedes the previous Jetski loader assessment in
4
+ `security-findings-triage-2026-03-15.md`.
5
+
6
+ ## Correction
7
+
8
+ - Finding: `Example loader trusts manifest paths, enabling file read`
9
+ - Path: `docs/integrations/jetski-gemini-loader/loader.ts`
10
+ - Previous triage status on 2026-03-15: `obsolete/not reproducible on current HEAD`
11
+ - Corrected assessment: the loader was still reproducible via a symlinked
12
+ `SKILL.md` that resolved outside `skillsRoot`. A local proof read the linked
13
+ file contents successfully.
14
+
15
+ ## Current Status
16
+
17
+ - The loader now rejects symlinked skill directories and symlinked `SKILL.md`
18
+ files.
19
+ - The loader now resolves the real path for `SKILL.md` and rejects any target
20
+ outside the configured `skillsRoot`.
21
+ - Regression coverage lives in
22
+ `tools/scripts/tests/jetski_gemini_loader.test.js`.
@@ -793,7 +793,7 @@ public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
793
793
 
794
794
  ## Resources
795
795
 
796
- - **assets/service-template.cs**: Complete service implementation template
797
- - **assets/repository-template.cs**: Repository pattern implementation
796
+ - **assets/service-template.cs.template**: Complete service implementation template
797
+ - **assets/repository-template.cs.template**: Repository pattern implementation
798
798
  - **references/ef-core-best-practices.md**: EF Core optimization guide
799
799
  - **references/dapper-patterns.md**: Advanced Dapper usage patterns
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import argparse
21
21
  import asyncio
22
+ import html
22
23
  import json
23
24
  import os
24
25
  import sys
@@ -47,6 +48,15 @@ db = Database()
47
48
  db.init()
48
49
 
49
50
 
51
+ def _mask_secret(value: str, keep: int = 4) -> str:
52
+ """Mask secret-like values before showing them in terminal output."""
53
+ if not value:
54
+ return "(hidden)"
55
+ if len(value) <= keep:
56
+ return "*" * len(value)
57
+ return f"{value[:keep]}...masked"
58
+
59
+
50
60
  # ── OAuth Callback Server ────────────────────────────────────────────────────
51
61
 
52
62
  class OAuthCallbackHandler(BaseHTTPRequestHandler):
@@ -71,7 +81,8 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
71
81
  self.send_response(400)
72
82
  self.send_header("Content-Type", "text/html; charset=utf-8")
73
83
  self.end_headers()
74
- self.wfile.write(f"<html><body><h2>Erro: {error}</h2></body></html>".encode())
84
+ safe_error = html.escape(error, quote=True)
85
+ self.wfile.write(f"<html><body><h2>Erro: {safe_error}</h2></body></html>".encode())
75
86
  else:
76
87
  self.send_response(404)
77
88
  self.end_headers()
@@ -84,7 +95,7 @@ def wait_for_oauth_code() -> Optional[str]:
84
95
  """Inicia servidor local e espera pelo código de autorização."""
85
96
  server = HTTPServer(("localhost", OAUTH_REDIRECT_PORT), OAuthCallbackHandler)
86
97
  server.timeout = 120 # 2 minutos
87
- print(f"Aguardando autorização em http://localhost:{OAUTH_REDIRECT_PORT}/callback ...")
98
+ print("Aguardando autorização no callback OAuth local...")
88
99
  print("(Timeout: 2 minutos)\n")
89
100
 
90
101
  while OAuthCallbackHandler.authorization_code is None:
@@ -285,10 +296,8 @@ async def setup() -> None:
285
296
  f"response_type=code"
286
297
  )
287
298
 
288
- print(f"\nAbrindo browser para autorização...")
289
- # Mask client_id in auth URL to avoid logging credentials
290
- masked_url = auth_url.replace(app_id, app_id[:4] + "...masked") if app_id else auth_url
291
- print(f"URL: {masked_url}\n")
299
+ print("\nAbrindo browser para autorização...")
300
+ print("A URL de autorização e o App ID não serão exibidos para evitar vazamento de credenciais.\n")
292
301
  webbrowser.open(auth_url)
293
302
 
294
303
  # Esperar callback