-
Notifications
You must be signed in to change notification settings - Fork 0
feat(js-pr-validation): add socket.dev supply chain gate #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4693ef8
cfe360f
f75d2fd
ee22bc5
77b3ad2
5b9ecc6
54f3c4d
bb8ca83
cde744a
7a33a1e
ef1c887
a778bc2
13d8d48
4a53572
4f08039
de9c860
43ac207
b9f61ce
52c6a81
651aa00
0f1afd5
e190d16
d5fa617
eacae6a
a301f93
817e47d
3ce2df2
b6365a2
6137cc4
ce54891
b38f781
87fffd0
b38a179
c7ad918
f850c85
7d39f39
10b45f7
95c6f70
aaf144c
c807994
b1aa4f4
81cc6bf
7b3255f
9f96d77
cbaa29e
471eb23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,6 +37,10 @@ on: | |||||||||||||||
| description: 'Run the security scan pipeline (Trivy, CodeQL, prerelease checks)' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| run_socket: | ||||||||||||||||
| description: 'Run the Socket supply-chain pipeline (Socket Firewall and, when enabled, the Socket CLI scan)' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
|
Comment on lines
+40
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Wire the paid Socket scan or remove the public claim. The
📍 Affects 2 files
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| # ----------------- Change gate ----------------- | ||||||||||||||||
| ignore_globs: | ||||||||||||||||
|
|
@@ -282,14 +286,111 @@ on: | |||||||||||||||
| description: 'Comma-separated directories to skip in every Trivy filesystem scan (appended to the built-in skip list).' | ||||||||||||||||
| type: string | ||||||||||||||||
| default: '' | ||||||||||||||||
|
|
||||||||||||||||
| # ----------------- Socket supply chain (src/security/socket-*) ----------------- | ||||||||||||||||
| socket_enable_firewall: | ||||||||||||||||
| description: 'Run Socket Firewall (free tier, no token) and install dependencies through it, blocking malicious packages at install time.' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_working_dir: | ||||||||||||||||
| description: 'Directory holding the package.json and lockfile scanned by the Socket jobs' | ||||||||||||||||
| type: string | ||||||||||||||||
| default: '.' | ||||||||||||||||
| socket_firewall_version: | ||||||||||||||||
| description: 'Socket Firewall binary version. Empty or "latest" tracks the newest release.' | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'latest' | ||||||||||||||||
| socket_job_summary: | ||||||||||||||||
| description: 'Socket Firewall job summary verbosity (all, errors, none)' | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'all' | ||||||||||||||||
| socket_use_cache: | ||||||||||||||||
| description: 'Cache the sfw binary between runs. Unrelated to the package-manager cache, which is always purged before the guarded install.' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_fail_on_block: | ||||||||||||||||
| description: 'Fail the Socket job when Socket Firewall blocks a package. Set to false to report the block as a warning instead.' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_enable_app_gate: | ||||||||||||||||
| description: >- | ||||||||||||||||
| Turn the Socket GitHub App checks into an enforceable gate. The App analyses the dependency graph and | ||||||||||||||||
| posts advisory checks; this makes an adverse verdict block the pull request. Needs no token. | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_app_slug: | ||||||||||||||||
| description: 'GitHub App slug whose checks the gate reads' | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'socket-security' | ||||||||||||||||
| socket_app_timeout: | ||||||||||||||||
| description: 'Seconds to wait for the Socket App checks to complete before treating the result as inconclusive' | ||||||||||||||||
| type: number | ||||||||||||||||
| default: 300 | ||||||||||||||||
|
bedatty marked this conversation as resolved.
|
||||||||||||||||
| socket_app_fail_on_findings: | ||||||||||||||||
| description: 'Fail the Socket job when the App reports adverse checks' | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_app_on_inconclusive: | ||||||||||||||||
| description: >- | ||||||||||||||||
| What to do when the App ran but reached no verdict (neutral/skipped/timeout): "block" (default) or "warn". | ||||||||||||||||
| A conflicted pull request is the common cause, and it must not read as clean. | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'block' | ||||||||||||||||
| socket_enable_api_report: | ||||||||||||||||
| description: >- | ||||||||||||||||
| Read the full scan the Socket App already computed and report per-package alerts, vulnerabilities and | ||||||||||||||||
| scores in the pull request comment. Requires SOCKET_SECURITY_API_KEY with the full-scans:list scope. | ||||||||||||||||
| Advisory only — it never gates the merge. | ||||||||||||||||
| type: boolean | ||||||||||||||||
| default: true | ||||||||||||||||
| socket_api_max_rows: | ||||||||||||||||
| description: 'Maximum package rows kept in the dependency findings table' | ||||||||||||||||
| type: number | ||||||||||||||||
| default: 25 | ||||||||||||||||
| socket_api_include_actions: | ||||||||||||||||
| description: >- | ||||||||||||||||
| Comma-separated Socket alert actions reported as findings. Socket marks roughly 99% of alerts as "ignore" | ||||||||||||||||
| (capability observations such as envVars), so reporting unfiltered buries a handful of findings in | ||||||||||||||||
| thousands of lines. | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'error,warn,monitor' | ||||||||||||||||
| socket_comment_when: | ||||||||||||||||
| description: >- | ||||||||||||||||
| "findings" (the default) posts the Socket comment only when there is something to act on. "always" posts | ||||||||||||||||
| every run. Operational state — whether the scan ran, which checks passed — is never in the comment; it | ||||||||||||||||
| lives in the job log and the Socket status check. | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'findings' | ||||||||||||||||
| socket_api_fail_on_actions: | ||||||||||||||||
| description: >- | ||||||||||||||||
| Comma-separated Socket alert actions that block the pull request. Empty (the default) reports without | ||||||||||||||||
| blocking. Set to e.g. "error" to own the merge decision in this workflow rather than deferring it to the | ||||||||||||||||
| Socket App's verdict. | ||||||||||||||||
| type: string | ||||||||||||||||
| default: '' | ||||||||||||||||
|
Comment on lines
+364
to
+370
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document configured API findings as blocking. A non-empty
📍 Affects 2 files
🤖 Prompt for AI Agents |
||||||||||||||||
| socket_app_on_missing: | ||||||||||||||||
| description: >- | ||||||||||||||||
| What to do when the App published no checks, which is what a repository without it installed looks like: | ||||||||||||||||
| "warn" (default) keeps those repositories green, "block" requires the App. | ||||||||||||||||
| type: string | ||||||||||||||||
| default: 'warn' | ||||||||||||||||
| secrets: | ||||||||||||||||
| MANAGE_TOKEN: | ||||||||||||||||
| required: false | ||||||||||||||||
| SLACK_WEBHOOK_URL: | ||||||||||||||||
| required: false | ||||||||||||||||
| # Declared but not consumed by any job today: the Socket layers here run | ||||||||||||||||
| # token-free (the firewall inspects traffic locally, the App gate reads the | ||||||||||||||||
| # GitHub checks API). It is declared so an organization secret reaches this | ||||||||||||||||
| # workflow through `secrets: inherit` without a release, which a reusable | ||||||||||||||||
| # workflow cannot do for an undeclared secret. Do not delete as dead — | ||||||||||||||||
| # Socket Firewall enterprise is the intended consumer. | ||||||||||||||||
| SOCKET_SECURITY_API_KEY: | ||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||
| required: false | ||||||||||||||||
|
|
||||||||||||||||
| permissions: | ||||||||||||||||
| actions: read | ||||||||||||||||
| checks: read | ||||||||||||||||
|
Comment on lines
391
to
+393
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restore The workflow-level block removes Proposed fix permissions:
actions: read
checks: read
+ contents: read📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| contents: read | ||||||||||||||||
| id-token: write | ||||||||||||||||
| issues: write | ||||||||||||||||
|
|
@@ -351,6 +452,9 @@ jobs: | |||||||||||||||
| path_level: ${{ inputs.path_level }} | ||||||||||||||||
| normalize_to_filter: ${{ inputs.normalize_to_filter }} | ||||||||||||||||
| app_name_prefix: ${{ inputs.app_name_prefix }} | ||||||||||||||||
| enable_socket_firewall: ${{ inputs.socket_enable_firewall }} | ||||||||||||||||
| socket_firewall_version: ${{ inputs.socket_firewall_version }} | ||||||||||||||||
| socket_fail_on_block: ${{ inputs.socket_fail_on_block }} | ||||||||||||||||
| enable_lint: ${{ inputs.enable_lint }} | ||||||||||||||||
| enable_typecheck: ${{ inputs.enable_typecheck }} | ||||||||||||||||
| enable_security: ${{ inputs.enable_security }} | ||||||||||||||||
|
|
@@ -427,6 +531,169 @@ jobs: | |||||||||||||||
| result: ${{ needs.changes.result != 'success' && needs.changes.result || needs.security.result }} | ||||||||||||||||
| label: Security | ||||||||||||||||
|
|
||||||||||||||||
| # ----------------- Socket Supply Chain ----------------- | ||||||||||||||||
| socket: | ||||||||||||||||
| name: Socket (checks) | ||||||||||||||||
| needs: changes | ||||||||||||||||
| if: inputs.run_socket && needs.changes.outputs.code == 'true' | ||||||||||||||||
| runs-on: ${{ vars.GENERAL_RUNNERS || inputs.runner_type }} | ||||||||||||||||
| permissions: | ||||||||||||||||
| contents: read | ||||||||||||||||
| checks: read | ||||||||||||||||
| issues: write | ||||||||||||||||
| pull-requests: write | ||||||||||||||||
| env: | ||||||||||||||||
| SOCKET_APP_NAME: ${{ inputs.app_name_prefix != '' && inputs.app_name_prefix || github.event.repository.name }} | ||||||||||||||||
| steps: | ||||||||||||||||
| - name: Checkout code | ||||||||||||||||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 | ||||||||||||||||
| with: | ||||||||||||||||
| persist-credentials: false | ||||||||||||||||
|
|
||||||||||||||||
| # This install is the reporting source of truth for the Socket Firewall | ||||||||||||||||
| # row below. Enforcement itself is broader: every install in | ||||||||||||||||
| # frontend-pr-analysis.yml also runs through the firewall, so a malicious | ||||||||||||||||
| # package cannot execute install scripts in any analysis job either. | ||||||||||||||||
| - name: Socket Firewall | ||||||||||||||||
| id: firewall | ||||||||||||||||
| if: inputs.socket_enable_firewall | ||||||||||||||||
| continue-on-error: true | ||||||||||||||||
| uses: LerianStudio/github-actions-shared-workflows/src/security/socket-firewall@v1 | ||||||||||||||||
| with: | ||||||||||||||||
| package-manager: ${{ inputs.package_manager }} | ||||||||||||||||
| node-version: ${{ inputs.node_version }} | ||||||||||||||||
| working-dir: ${{ inputs.socket_working_dir }} | ||||||||||||||||
| firewall-version: ${{ inputs.socket_firewall_version }} | ||||||||||||||||
| job-summary: ${{ inputs.socket_job_summary }} | ||||||||||||||||
| use-cache: ${{ inputs.socket_use_cache }} | ||||||||||||||||
| fail-on-block: ${{ inputs.socket_fail_on_block }} | ||||||||||||||||
| dry-run: ${{ inputs.dry_run }} | ||||||||||||||||
|
|
||||||||||||||||
| - name: Socket App Gate | ||||||||||||||||
| id: app-gate | ||||||||||||||||
| if: inputs.socket_enable_app_gate && github.event_name == 'pull_request' | ||||||||||||||||
| continue-on-error: true | ||||||||||||||||
| uses: LerianStudio/github-actions-shared-workflows/src/security/socket-app-gate@v1 | ||||||||||||||||
| with: | ||||||||||||||||
| github-token: ${{ secrets.MANAGE_TOKEN || github.token }} | ||||||||||||||||
| # The App reports against the pull request head, not the merge commit. | ||||||||||||||||
| commit-sha: ${{ github.event.pull_request.head.sha }} | ||||||||||||||||
| app-slug: ${{ inputs.socket_app_slug }} | ||||||||||||||||
| timeout-seconds: ${{ inputs.socket_app_timeout }} | ||||||||||||||||
| fail-on-findings: ${{ inputs.socket_app_fail_on_findings }} | ||||||||||||||||
| on-inconclusive: ${{ inputs.socket_app_on_inconclusive }} | ||||||||||||||||
| on-missing-app: ${{ inputs.socket_app_on_missing }} | ||||||||||||||||
|
|
||||||||||||||||
| # Reads the scan the App already produced, addressed by the dashboard URL the | ||||||||||||||||
| # gate captured — no new scan, one quota unit, no duplicate analysis. | ||||||||||||||||
| # The App publishes the diff scan id in its own comment. Looking it up by | ||||||||||||||||
| # after_full_scan_id does not work: the App diffs against a different full | ||||||||||||||||
| # scan than the one its Project Report check links to. | ||||||||||||||||
| - name: Resolve Socket diff scan id | ||||||||||||||||
| id: diff-scan | ||||||||||||||||
| if: always() && inputs.socket_enable_api_report && github.event_name == 'pull_request' | ||||||||||||||||
| continue-on-error: true | ||||||||||||||||
| env: | ||||||||||||||||
| GH_TOKEN: ${{ secrets.MANAGE_TOKEN || github.token }} | ||||||||||||||||
| PR: ${{ github.event.pull_request.number }} | ||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||
| run: | | ||||||||||||||||
| JQ='[.[] | select(.user.login == "socket-security[bot]") | .body] | last // ""' | ||||||||||||||||
| BODY=$(gh api "repos/$REPO/issues/$PR/comments" --paginate --jq "$JQ") | ||||||||||||||||
| ID=$(printf '%s' "$BODY" \ | ||||||||||||||||
| | grep -oE 'diff-scan/[0-9a-fA-F-]{36}' | head -1 | cut -d/ -f2 || true) | ||||||||||||||||
| if [ -n "$ID" ]; then | ||||||||||||||||
| echo "Resolved diff scan $ID from the Socket App comment." | ||||||||||||||||
| else | ||||||||||||||||
| echo "No diff scan id in the Socket App comment — it only posts one when dependencies change." | ||||||||||||||||
| fi | ||||||||||||||||
| echo "id=$ID" >> "$GITHUB_OUTPUT" | ||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||
|
|
||||||||||||||||
| - name: Socket API Report | ||||||||||||||||
| id: api-report | ||||||||||||||||
| if: always() && inputs.socket_enable_api_report && github.event_name == 'pull_request' | ||||||||||||||||
| continue-on-error: true | ||||||||||||||||
| uses: LerianStudio/github-actions-shared-workflows/src/security/socket-api-report@v1 | ||||||||||||||||
| with: | ||||||||||||||||
| socket-api-key: ${{ secrets.SOCKET_SECURITY_API_KEY }} | ||||||||||||||||
| report-url: ${{ steps.app-gate.outputs.report-url }} | ||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||
| max-rows: ${{ inputs.socket_api_max_rows }} | ||||||||||||||||
| include-actions: ${{ inputs.socket_api_include_actions }} | ||||||||||||||||
| fail-on-actions: ${{ inputs.socket_api_fail_on_actions }} | ||||||||||||||||
| # The baseline: findings already on the target branch are reported as | ||||||||||||||||
| # pre-existing and never block, so a repository carrying debt does not | ||||||||||||||||
| # fail every pull request for something none of them caused. | ||||||||||||||||
| base-branch: ${{ github.base_ref }} | ||||||||||||||||
| head-sha: ${{ github.event.pull_request.head.sha }} | ||||||||||||||||
| diff-scan-id: ${{ steps.diff-scan.outputs.id }} | ||||||||||||||||
|
|
||||||||||||||||
| - name: Post Socket report to PR | ||||||||||||||||
| if: always() && github.event_name == 'pull_request' && !inputs.dry_run | ||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||
| uses: LerianStudio/github-actions-shared-workflows/src/security/socket-reporter@v1 | ||||||||||||||||
| with: | ||||||||||||||||
| github-token: ${{ secrets.MANAGE_TOKEN || github.token }} | ||||||||||||||||
| app-name: ${{ env.SOCKET_APP_NAME }} | ||||||||||||||||
| comment-when: ${{ inputs.socket_comment_when }} | ||||||||||||||||
| firewall-blocked: ${{ steps.firewall.outputs.blocked || 'false' }} | ||||||||||||||||
| firewall-fail-on-block: ${{ inputs.socket_fail_on_block }} | ||||||||||||||||
| firewall-findings-file: ${{ steps.firewall.outputs.findings-file }} | ||||||||||||||||
| api-findings-file: ${{ steps.api-report.outputs.findings-file }} | ||||||||||||||||
| api-blocking-count: ${{ steps.api-report.outputs.blocking-count || '0' }} | ||||||||||||||||
| # yamllint disable-line rule:line-length | ||||||||||||||||
| run-url: ${{ format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | ||||||||||||||||
|
|
||||||||||||||||
| # continue-on-error above keeps the report reachable even when a layer | ||||||||||||||||
| # fails, so the verdict is re-applied here. | ||||||||||||||||
| # dry_run is a preview across the whole umbrella, so the gate reports the | ||||||||||||||||
| # verdict it would have applied instead of applying it. | ||||||||||||||||
| - name: Gate - Fail on Socket findings | ||||||||||||||||
| if: always() && !inputs.dry_run | ||||||||||||||||
| env: | ||||||||||||||||
| FIREWALL_OUTCOME: ${{ steps.firewall.outcome }} | ||||||||||||||||
| APP_GATE_OUTCOME: ${{ steps.app-gate.outcome }} | ||||||||||||||||
| API_BLOCKING: ${{ steps.api-report.outputs.blocking-count }} | ||||||||||||||||
| run: | | ||||||||||||||||
| FAILED="" | ||||||||||||||||
| [ "$FIREWALL_OUTCOME" = "failure" ] && FAILED="$FAILED Socket-Firewall" | ||||||||||||||||
| [ "$APP_GATE_OUTCOME" = "failure" ] && FAILED="$FAILED Socket-App-Gate" | ||||||||||||||||
| # Only real findings from the API layer block. Its API-failure paths | ||||||||||||||||
| # report zero here by construction, so an unreachable Socket cannot | ||||||||||||||||
| # fail a pull request. | ||||||||||||||||
| if [ "${API_BLOCKING:-0}" -gt 0 ] 2>/dev/null; then | ||||||||||||||||
| FAILED="$FAILED Socket-Dependency-Findings" | ||||||||||||||||
| fi | ||||||||||||||||
| if [ -n "$FAILED" ]; then | ||||||||||||||||
| echo "::error::Socket supply chain gate failed:$FAILED. See the Socket comment on the pull request." | ||||||||||||||||
| exit 1 | ||||||||||||||||
| fi | ||||||||||||||||
| echo "Socket supply chain gate passed." | ||||||||||||||||
|
|
||||||||||||||||
| - name: Gate - Socket findings (dry run) | ||||||||||||||||
| if: always() && inputs.dry_run | ||||||||||||||||
| env: | ||||||||||||||||
| FIREWALL_OUTCOME: ${{ steps.firewall.outcome }} | ||||||||||||||||
| APP_GATE_OUTCOME: ${{ steps.app-gate.outcome }} | ||||||||||||||||
| API_BLOCKING: ${{ steps.api-report.outputs.blocking-count }} | ||||||||||||||||
| run: | | ||||||||||||||||
| echo "::notice::DRY RUN — Socket gate not enforced" | ||||||||||||||||
| echo " Socket Firewall : $FIREWALL_OUTCOME" | ||||||||||||||||
| echo " Socket App gate : $APP_GATE_OUTCOME" | ||||||||||||||||
| echo " Blocking findings: ${API_BLOCKING:-0}" | ||||||||||||||||
|
|
||||||||||||||||
| socket-gate: | ||||||||||||||||
| name: Socket | ||||||||||||||||
| needs: [changes, socket] | ||||||||||||||||
| if: always() | ||||||||||||||||
| runs-on: ${{ vars.GENERAL_RUNNERS || inputs.runner_type }} | ||||||||||||||||
| permissions: | ||||||||||||||||
| contents: read | ||||||||||||||||
| steps: | ||||||||||||||||
| - name: Aggregate Socket result | ||||||||||||||||
| uses: LerianStudio/github-actions-shared-workflows/src/validate/result-gate@v1 | ||||||||||||||||
| with: | ||||||||||||||||
| result: ${{ needs.changes.result != 'success' && needs.changes.result || needs.socket.result }} | ||||||||||||||||
| label: Socket | ||||||||||||||||
|
|
||||||||||||||||
| # ----------------- Org ruleset compatibility stubs ----------------- | ||||||||||||||||
| # Go Analysis and Lib Version are required status checks in the Lerian org | ||||||||||||||||
| # branch-protection ruleset (designed for Go repos). These stubs are | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: LerianStudio/github-actions-shared-workflows
Length of output: 8631
🏁 Script executed:
Repository: LerianStudio/github-actions-shared-workflows
Length of output: 35084
🏁 Script executed:
Repository: LerianStudio/github-actions-shared-workflows
Length of output: 50400
🏁 Script executed:
Repository: LerianStudio/github-actions-shared-workflows
Length of output: 9610
Add an active update path for
SocketDev/action.Dependabot does not scan composite actions under
src/**/action.yml. The existingpinned-actionsvalidator checks pin format but does not update action versions. Bothsrc/security/socket-firewall/action.ymlandsrc/setup/setup-node-guarded/action.ymlreferenceSocketDev/action. Add a drift check that covers both files.🤖 Prompt for AI Agents