Spaces:
Sleeping
Sleeping
OpenCode commited on
Commit ·
cd455f6
1
Parent(s): ac6c51b
Deploy latest SIN-GitHub-Issues runtime
Browse files- .env.example +47 -0
- .gitattributes +0 -35
- .github/workflows/deploy-huggingface-space.yml +7 -1
- .gitignore +1 -1
- .well-known/agent-card.json +78 -0
- .well-known/oauth-client.json +1 -1
- A2A-CARD.md +19 -25
- AGENTS.md +2 -3
- README.md +52 -15
- agent.json +3 -2
- config/github-app-routing.example.json +106 -0
- package-lock.json +154 -2
- package.json +11 -3
- scripts/check-github-app-routing.mjs +113 -0
- scripts/check-hf-deploy-prereqs.mjs +79 -0
- scripts/comment-as-github-app.mjs +57 -0
- scripts/hf_pull_script.py +1 -0
- scripts/init-github-app-routing-local.mjs +54 -0
- scripts/render-github-app-env-template.mjs +102 -0
- scripts/render-github-app-vm-rollout.mjs +103 -0
- scripts/smoke-github-app-routing.mjs +99 -0
- scripts/smoke-room13-worker.mjs +0 -45
- scripts/smoke-triage-fixtures.mjs +0 -85
- src/a2a-http.ts +31 -7
- src/cli.ts +1 -12
- src/github-app-routing.ts +364 -0
- src/hf_sync.ts +0 -86
- src/idle-monetization.ts +0 -33
- src/lib/telemetry/ZeroTrustAuth.ts +62 -0
- src/mcp-server.ts +89 -121
- src/metadata.ts +17 -22
- src/queue-state.js +0 -29
- src/queue-state.ts +0 -31
- src/room13-worker.ts +0 -191
- src/runtime.ts +103 -186
- src/self-healing.ts +0 -32
- src/triage.ts +0 -100
- test/github_workflow.test.mjs +156 -0
.env.example
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SIN_GITHUB_ISSUES_PUBLIC_BASE_URL=http://127.0.0.1:46031
|
| 2 |
+
SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json
|
| 3 |
+
SIN_GITHUB_API_BASE_URL=https://api.github.com
|
| 4 |
+
# Optional inline override:
|
| 5 |
+
# SIN_GITHUB_APP_ROUTING_JSON={"mode":"central-endpoint-routing","defaultWebhookPath":"/api/webhooks/github-apps","apps":[]}
|
| 6 |
+
|
| 7 |
+
# Example secret env vars referenced by config/github-app-routing.local.json
|
| 8 |
+
SIN_HERMES_GITHUB_APP_ID=
|
| 9 |
+
SIN_HERMES_GITHUB_APP_CLIENT_ID=
|
| 10 |
+
SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 11 |
+
SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET=
|
| 12 |
+
SIN_ZEUS_GITHUB_APP_ID=
|
| 13 |
+
SIN_ZEUS_GITHUB_APP_CLIENT_ID=
|
| 14 |
+
SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 15 |
+
SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET=
|
| 16 |
+
SIN_BUGBOUNTY_GITHUB_APP_ID=
|
| 17 |
+
SIN_BUGBOUNTY_GITHUB_APP_CLIENT_ID=
|
| 18 |
+
SIN_BUGBOUNTY_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 19 |
+
SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET=
|
| 20 |
+
SIN_GITHUB_ISSUES_GITHUB_APP_ID=
|
| 21 |
+
SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID=
|
| 22 |
+
SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 23 |
+
SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET=
|
| 24 |
+
SIN_BACKEND_GITHUB_APP_ID=
|
| 25 |
+
SIN_BACKEND_GITHUB_APP_CLIENT_ID=
|
| 26 |
+
SIN_BACKEND_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 27 |
+
SIN_BACKEND_GITHUB_APP_WEBHOOK_SECRET=
|
| 28 |
+
SIN_FRONTEND_GITHUB_APP_ID=
|
| 29 |
+
SIN_FRONTEND_GITHUB_APP_CLIENT_ID=
|
| 30 |
+
SIN_FRONTEND_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 31 |
+
SIN_FRONTEND_GITHUB_APP_WEBHOOK_SECRET=
|
| 32 |
+
SIN_TESTER_GITHUB_APP_ID=
|
| 33 |
+
SIN_TESTER_GITHUB_APP_CLIENT_ID=
|
| 34 |
+
SIN_TESTER_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 35 |
+
SIN_TESTER_GITHUB_APP_WEBHOOK_SECRET=
|
| 36 |
+
SIN_CODE_DATABASE_GITHUB_APP_ID=
|
| 37 |
+
SIN_CODE_DATABASE_GITHUB_APP_CLIENT_ID=
|
| 38 |
+
SIN_CODE_DATABASE_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 39 |
+
SIN_CODE_DATABASE_GITHUB_APP_WEBHOOK_SECRET=
|
| 40 |
+
SIN_CODE_INTEGRATION_GITHUB_APP_ID=
|
| 41 |
+
SIN_CODE_INTEGRATION_GITHUB_APP_CLIENT_ID=
|
| 42 |
+
SIN_CODE_INTEGRATION_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 43 |
+
SIN_CODE_INTEGRATION_GITHUB_APP_WEBHOOK_SECRET=
|
| 44 |
+
SIN_CODE_AI_GITHUB_APP_ID=
|
| 45 |
+
SIN_CODE_AI_GITHUB_APP_CLIENT_ID=
|
| 46 |
+
SIN_CODE_AI_GITHUB_APP_PRIVATE_KEY_PEM=
|
| 47 |
+
SIN_CODE_AI_GITHUB_APP_WEBHOOK_SECRET=
|
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/deploy-huggingface-space.yml
CHANGED
|
@@ -7,12 +7,18 @@ on:
|
|
| 7 |
|
| 8 |
jobs:
|
| 9 |
deploy:
|
| 10 |
-
if: ${{ secrets.HF_TOKEN != '' }}
|
| 11 |
runs-on: ubuntu-latest
|
| 12 |
env:
|
| 13 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 14 |
HF_SPACE_REPO_ID: delqhi/sin-github-issues
|
| 15 |
steps:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
- name: Checkout
|
| 17 |
uses: actions/checkout@v4
|
| 18 |
|
|
|
|
| 7 |
|
| 8 |
jobs:
|
| 9 |
deploy:
|
|
|
|
| 10 |
runs-on: ubuntu-latest
|
| 11 |
env:
|
| 12 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 13 |
HF_SPACE_REPO_ID: delqhi/sin-github-issues
|
| 14 |
steps:
|
| 15 |
+
- name: Ensure HF token is configured
|
| 16 |
+
run: |
|
| 17 |
+
if [ -z "${HF_TOKEN}" ]; then
|
| 18 |
+
echo "HF_TOKEN secret is missing"
|
| 19 |
+
exit 1
|
| 20 |
+
fi
|
| 21 |
+
|
| 22 |
- name: Checkout
|
| 23 |
uses: actions/checkout@v4
|
| 24 |
|
.gitignore
CHANGED
|
@@ -3,5 +3,5 @@ node_modules
|
|
| 3 |
*.log
|
| 4 |
.env
|
| 5 |
.env.local
|
|
|
|
| 6 |
.DS_Store
|
| 7 |
-
|
|
|
|
| 3 |
*.log
|
| 4 |
.env
|
| 5 |
.env.local
|
| 6 |
+
config/github-app-routing.local.json
|
| 7 |
.DS_Store
|
|
|
.well-known/agent-card.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "SIN-GitHub-Issues",
|
| 3 |
+
"description": "CEO Elite GitHub Operations Architect — orchestrates Projects V2, Issues, Wikis, Discussions, Gists, Security audits, PR Reviews, and public Showcase Ledger logging.",
|
| 4 |
+
"version": "2026.03.22",
|
| 5 |
+
"documentationUrl": "http://127.0.0.1:46031",
|
| 6 |
+
"url": "http://127.0.0.1:46031/a2a/v1",
|
| 7 |
+
"capabilities": {
|
| 8 |
+
"streaming": false,
|
| 9 |
+
"pushNotifications": false
|
| 10 |
+
},
|
| 11 |
+
"defaultInputModes": [
|
| 12 |
+
"text/plain",
|
| 13 |
+
"application/json"
|
| 14 |
+
],
|
| 15 |
+
"defaultOutputModes": [
|
| 16 |
+
"text/plain",
|
| 17 |
+
"application/json"
|
| 18 |
+
],
|
| 19 |
+
"skills": [
|
| 20 |
+
{
|
| 21 |
+
"id": "sin.github.health",
|
| 22 |
+
"name": "Health",
|
| 23 |
+
"description": "Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities."
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "sin.github.project.orchestrate",
|
| 27 |
+
"name": "Project Orchestrate",
|
| 28 |
+
"description": "Create and manage GitHub Projects V2 boards, link issues, and assign items."
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "sin.github.issue.manage",
|
| 32 |
+
"name": "Issue Manage",
|
| 33 |
+
"description": "Create, update, label, assign, milestone-link, or close GitHub issues and Epics."
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"id": "sin.github.wiki.sync",
|
| 37 |
+
"name": "Wiki Sync",
|
| 38 |
+
"description": "Synchronize repository Wiki pages with project documentation updates."
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "sin.github.discussion.start",
|
| 42 |
+
"name": "Discussion Start",
|
| 43 |
+
"description": "Start or manage GitHub Discussions for architecture debates and design decisions."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "sin.github.gist.publish",
|
| 47 |
+
"name": "Gist Publish",
|
| 48 |
+
"description": "Upload long logs, code snippets, or configs to GitHub Gists and return the URL."
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": "sin.github.security.audit",
|
| 52 |
+
"name": "Security Audit",
|
| 53 |
+
"description": "Audit Dependabot alerts, configure CodeQL, and review active security advisories."
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"id": "sin.github.pr.review",
|
| 57 |
+
"name": "PR Review",
|
| 58 |
+
"description": "Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass."
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"id": "sin.github.ledger.log",
|
| 62 |
+
"name": "Ledger Log",
|
| 63 |
+
"description": "Publish an agent activity log or achievement to the public Delqhi/OpenSIN-Ledger showcase repository."
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "sin.github.issue.pool.enqueue",
|
| 67 |
+
"name": "Issue Pool Enqueue",
|
| 68 |
+
"description": "Insert GitHub issue metadata into `sin_issues_pool` for Hermes/Team-Coding dispatch."
|
| 69 |
+
}
|
| 70 |
+
],
|
| 71 |
+
"supportedInterfaces": [
|
| 72 |
+
{
|
| 73 |
+
"url": "http://127.0.0.1:46031/a2a/v1",
|
| 74 |
+
"protocolBinding": "JSONRPC",
|
| 75 |
+
"protocolVersion": "1.0"
|
| 76 |
+
}
|
| 77 |
+
]
|
| 78 |
+
}
|
.well-known/oauth-client.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"client_id": "https://delqhi-sin-github-issues.hf.space/.well-known/oauth-client.json",
|
| 3 |
-
"client_name": "
|
| 4 |
"redirect_uris": ["https://delqhi-sin-github-issues.hf.space/oauth/callback"],
|
| 5 |
"grant_types": ["authorization_code"],
|
| 6 |
"response_types": ["code"],
|
|
|
|
| 1 |
{
|
| 2 |
"client_id": "https://delqhi-sin-github-issues.hf.space/.well-known/oauth-client.json",
|
| 3 |
+
"client_name": "SIN-GitHub-Issues",
|
| 4 |
"redirect_uris": ["https://delqhi-sin-github-issues.hf.space/oauth/callback"],
|
| 5 |
"grant_types": ["authorization_code"],
|
| 6 |
"response_types": ["code"],
|
A2A-CARD.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
**Team:** Coding
|
| 4 |
**Version:** 2026.03.22
|
|
@@ -6,8 +6,8 @@
|
|
| 6 |
|
| 7 |
## Identity & Mandate
|
| 8 |
|
| 9 |
-
> **You are
|
| 10 |
-
> You do NOT just create bug tickets. You
|
| 11 |
>
|
| 12 |
> 1. **Projects (V2)**: You autonomously generate Kanban boards, Epics, Sprints, and assign tasks via the GitHub CLI (`gh project create/link/item-add`).
|
| 13 |
> 2. **Wikis**: You generate, maintain, and sync corporate knowledge base Markdown files to the repo's wiki (`sync-github-wiki.sh`).
|
|
@@ -23,34 +23,28 @@
|
|
| 23 |
## RPC Actions
|
| 24 |
|
| 25 |
- `sin.github.health`: GitHub Operations Architect readiness check.
|
| 26 |
-
- `sin.github.
|
| 27 |
-
- `sin.github.
|
| 28 |
-
- `sin.github.
|
| 29 |
-
- `sin.github.worker.checkpoint`: Send checkpoint progress for the current Room-13 task.
|
| 30 |
-
- `sin.github.worker.complete`: Mark the currently claimed Room-13 task as completed.
|
| 31 |
- `sin.github.project.orchestrate`: Create and manage GitHub Projects V2 boards.
|
| 32 |
- `sin.github.issue.manage`: Create, update, or close issues (Epics, Sub-tasks).
|
| 33 |
-
- `sin.github.issue.ensure`: Create or update an issue by exact-title match.
|
| 34 |
-
- `sin.github.issue.comment`: Add a GitHub issue comment.
|
| 35 |
-
- `sin.github.issue.close`: Close a GitHub issue with optional comment.
|
| 36 |
-
- `sin.github.issue.triage.enqueue`: Durably enqueue a GitHub issue triage event into the coder dispatch pool.
|
| 37 |
- `sin.github.wiki.sync`: Update the repository Wiki with project documentation.
|
| 38 |
-
|
| 39 |
-
## Ingress Contract
|
| 40 |
-
|
| 41 |
-
- Canonical enqueue payload example: `Docs/operations/phase22-github-issue-triage-payload.example.json`
|
| 42 |
-
- Replay-safe fixture examples:
|
| 43 |
-
- `Docs/operations/phase22-github-issue-opened.fixture.json`
|
| 44 |
-
- `Docs/operations/phase22-github-issue-opened.replay.fixture.json`
|
| 45 |
-
- Primary transport for instant triage: GitHub webhook -> n8n -> `sin.github.issue.triage.enqueue`
|
| 46 |
-
- Idempotency key policy: `delivery_id` first, then fallback `repo#issue_number:event_action`
|
| 47 |
-
- Queue state progression: `received -> queued -> claimed -> dispatched -> accepted -> completed|failed|dead_letter`
|
| 48 |
-
- Runtime queue constants live in `src/queue-state.ts` and are shared between ingress and Hermes dispatch logic.
|
| 49 |
-
- Route contract: ingress computes `assignedTeam`, `assignedAgentHint`, and `capabilityHint` from labels/title/body before Hermes dispatch.
|
| 50 |
-
- Local proof command: `npm run smoke:triage-fixtures`
|
| 51 |
- `sin.github.discussion.start`: Start an architectural debate in Discussions.
|
| 52 |
- `sin.github.gist.publish`: Upload a large log or code snippet to a Gist.
|
| 53 |
- `sin.github.security.audit`: Audit security alerts and configure Dependabot.
|
| 54 |
- `sin.github.pr.review`: Review and approve/merge Pull Requests.
|
|
|
|
| 55 |
|
| 56 |
## Data & Transport
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A2A-SIN-GitHub-Issues (CEO Elite GitHub Operations)
|
| 2 |
|
| 3 |
**Team:** Coding
|
| 4 |
**Version:** 2026.03.22
|
|
|
|
| 6 |
|
| 7 |
## Identity & Mandate
|
| 8 |
|
| 9 |
+
> **You are A2A-SIN-GitHub-Issues, a 2026 CEO-Level GitHub Operations Architect.**
|
| 10 |
+
> You do NOT just create bug tickets. You orchestrate the **entire repository lifecycle** using the full suite of GitHub's free Enterprise features:
|
| 11 |
>
|
| 12 |
> 1. **Projects (V2)**: You autonomously generate Kanban boards, Epics, Sprints, and assign tasks via the GitHub CLI (`gh project create/link/item-add`).
|
| 13 |
> 2. **Wikis**: You generate, maintain, and sync corporate knowledge base Markdown files to the repo's wiki (`sync-github-wiki.sh`).
|
|
|
|
| 23 |
## RPC Actions
|
| 24 |
|
| 25 |
- `sin.github.health`: GitHub Operations Architect readiness check.
|
| 26 |
+
- `sin.github.app.routing.status`: Inspect configured GitHub App routes, app IDs, and secret readiness.
|
| 27 |
+
- `sin.github.webhook.route`: Resolve an incoming GitHub webhook to the correct SIN coder app via `installation.app_id` and optional path routing.
|
| 28 |
+
- `sin.github.issue.comment.as_app`: Post an issue or PR comment as the matched GitHub App identity using JWT + installation token auth.
|
|
|
|
|
|
|
| 29 |
- `sin.github.project.orchestrate`: Create and manage GitHub Projects V2 boards.
|
| 30 |
- `sin.github.issue.manage`: Create, update, or close issues (Epics, Sub-tasks).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
- `sin.github.wiki.sync`: Update the repository Wiki with project documentation.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
- `sin.github.discussion.start`: Start an architectural debate in Discussions.
|
| 33 |
- `sin.github.gist.publish`: Upload a large log or code snippet to a Gist.
|
| 34 |
- `sin.github.security.audit`: Audit security alerts and configure Dependabot.
|
| 35 |
- `sin.github.pr.review`: Review and approve/merge Pull Requests.
|
| 36 |
+
- `sin.github.issue.pool.enqueue`: Insert an issue into `sin_issues_pool` for Hermes/Team-Coding dispatch.
|
| 37 |
|
| 38 |
## Data & Transport
|
| 39 |
+
|
| 40 |
+
JSON-RPC over HTTP. Strict OpenCode execution via `openai/gpt-5.4` utilizing the `hf_pull_script.py` rotator pool.
|
| 41 |
+
|
| 42 |
+
## GitHub App Routing
|
| 43 |
+
|
| 44 |
+
- Central webhook endpoint: `POST /github/webhook`
|
| 45 |
+
- Optional per-app endpoints: `POST /github/webhook/<agent-slug-or-route>`
|
| 46 |
+
- Routing key: `installation.app_id`
|
| 47 |
+
- Reply auth key: `installation.id`
|
| 48 |
+
- Supported config sources:
|
| 49 |
+
- `SIN_GITHUB_APP_ROUTING_PATH`
|
| 50 |
+
- `SIN_GITHUB_APP_ROUTING_JSON`
|
AGENTS.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
- Team: Team - Coding
|
| 4 |
- Team Manager: SIN-Coding-CEO
|
| 5 |
- Slug: sin-github-issues
|
| 6 |
-
- Canonical display name: AZA-SIN-Coder-•-GitHub-Issues
|
| 7 |
-
- Machine identity policy: keep slug/package/bin/public route as `sin-github-issues` until an explicit compatibility migration says otherwise
|
| 8 |
- Repo: https://github.com/Delqhi/sin-github-issues
|
| 9 |
- Docs Tab: pending dedicated child-tab
|
| 10 |
|
|
@@ -12,5 +10,6 @@ Arbeitsregel:
|
|
| 12 |
- Jede neue Action muss in `runtime.ts`, `mcp-server.ts` und `A2A-CARD.md` synchronisiert werden.
|
| 13 |
- **NEUE REGEL:** Der Agent ist kein dummer Ticketersteller. Lange Logs -> Gists. Architekturdebatten -> Discussions. Planung -> Projects. Doku -> Wikis. Code -> PRs.
|
| 14 |
- **PFLICHT:** `hf_pull_script.py` und `openai/gpt-5.4` MÜSSEN in der Runtime für OpenCode Commands genutzt werden. Kein Fallback auf alte lokale Auth-Tokens.
|
|
|
|
| 15 |
- Jeder neue Agent muss auch `agent.json`, `AGENTS.md`, `mcp-config.json` sauber besitzen.
|
| 16 |
- GitHub Actions Deploy Pipelines (`deploy-huggingface-space.yml`) pflegen.
|
|
|
|
| 1 |
+
# SIN-GitHub-Issues
|
| 2 |
|
| 3 |
- Team: Team - Coding
|
| 4 |
- Team Manager: SIN-Coding-CEO
|
| 5 |
- Slug: sin-github-issues
|
|
|
|
|
|
|
| 6 |
- Repo: https://github.com/Delqhi/sin-github-issues
|
| 7 |
- Docs Tab: pending dedicated child-tab
|
| 8 |
|
|
|
|
| 10 |
- Jede neue Action muss in `runtime.ts`, `mcp-server.ts` und `A2A-CARD.md` synchronisiert werden.
|
| 11 |
- **NEUE REGEL:** Der Agent ist kein dummer Ticketersteller. Lange Logs -> Gists. Architekturdebatten -> Discussions. Planung -> Projects. Doku -> Wikis. Code -> PRs.
|
| 12 |
- **PFLICHT:** `hf_pull_script.py` und `openai/gpt-5.4` MÜSSEN in der Runtime für OpenCode Commands genutzt werden. Kein Fallback auf alte lokale Auth-Tokens.
|
| 13 |
+
- Multi-App GitHub routing must use one GitHub App per specialized SIN coder, route incoming webhooks via `installation.app_id`, and use `installation.id` for the responding installation-token lane.
|
| 14 |
- Jeder neue Agent muss auch `agent.json`, `AGENTS.md`, `mcp-config.json` sauber besitzen.
|
| 15 |
- GitHub Actions Deploy Pipelines (`deploy-huggingface-space.yml`) pflegen.
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: "🤖"
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
@@ -8,23 +8,22 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
-
`
|
| 14 |
|
| 15 |
## Identity
|
| 16 |
|
| 17 |
-
- Slug: `sin-github-issues`
|
| 18 |
- Team: `Team - Coding`
|
| 19 |
- Team Manager: `SIN-Coding-CEO`
|
| 20 |
-
- Purpose: GitHub issue
|
| 21 |
-
- Usage: Use
|
| 22 |
-
-
|
| 23 |
-
- Primary Model: `opencode/nemotron-3-super-free`
|
| 24 |
|
| 25 |
## Deployment
|
| 26 |
|
| 27 |
-
- Local path: `/Users/jeremy/dev/
|
| 28 |
- GitHub repo: `https://github.com/Delqhi/sin-github-issues`
|
| 29 |
- Workforce index: `https://a2a.delqhi.com`
|
| 30 |
- Landing page: `https://delqhi-sin-github-issues.hf.space`
|
|
@@ -44,6 +43,48 @@ pinned: false
|
|
| 44 |
- best-effort `sin-supabase` audit persistence for workflow events
|
| 45 |
- canonical env names: `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` (legacy `SIN_SUPABASE_*` still accepted)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
## A2A Surface
|
| 48 |
|
| 49 |
- Card: `GET /.well-known/agent-card.json`
|
|
@@ -75,12 +116,8 @@ sin-github-issues run-action '{"action":"sin.github.issues.achievement.quickdraw
|
|
| 75 |
sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
|
| 76 |
sin-github-issues run-action '{"action":"sin.github.issues.issue.evidence.policy"}'
|
| 77 |
sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
|
| 78 |
-
sin-github-issues run-action '{"action":"sin.github.issues.runners.list","repo":"
|
| 79 |
-
sin-github-issues run-action '{"action":"sin.github.issues.workflow.runs.list","repo":"
|
| 80 |
-
sin-github-issues run-action '{"action":"sin.github.worker.register"}'
|
| 81 |
-
sin-github-issues run-action '{"action":"sin.github.worker.status"}'
|
| 82 |
-
sin-github-issues run-action '{"action":"sin.github.worker.claim_next"}'
|
| 83 |
-
cd /Users/jeremy/dev/SIN-Solver/a2a/team-coding/A2A-SIN-GitHub-Issues && npm run smoke:triage-fixtures
|
| 84 |
sin-github-issues run-action '{"action":"sin.github.issues.workflow.local_suite.run","repoPath":"/abs/repo","suite":"docs-and-architecture","confirm":true}'
|
| 85 |
```
|
| 86 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SIN-GitHub-Issues
|
| 3 |
emoji: "🤖"
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# SIN-GitHub-Issues
|
| 12 |
|
| 13 |
+
`SIN-GitHub-Issues` is a SIN A2A agent package for the Silicon Workforce.
|
| 14 |
|
| 15 |
## Identity
|
| 16 |
|
| 17 |
+
- Slug: `sin-github-issues`
|
| 18 |
- Team: `Team - Coding`
|
| 19 |
- Team Manager: `SIN-Coding-CEO`
|
| 20 |
+
- Purpose: GitHub issue execution agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff. Extended with enterprise profile management capabilities.
|
| 21 |
+
- Usage: Use after enterprise-deep-debug confirms a bug to search/create/update GitHub issues, open a repo branch, execute the fix workflow, commit, push, and report verification status back to the orchestrating CLI agent. For profile and achievement operations use the new profile/achievement actions.
|
| 22 |
+
- Primary Model: `openai/gpt-5.4`
|
|
|
|
| 23 |
|
| 24 |
## Deployment
|
| 25 |
|
| 26 |
+
- Local path: `/Users/jeremy/dev/OpenSIN-backend/a2a/team-coding/A2A-SIN-GitHub-Issues`
|
| 27 |
- GitHub repo: `https://github.com/Delqhi/sin-github-issues`
|
| 28 |
- Workforce index: `https://a2a.delqhi.com`
|
| 29 |
- Landing page: `https://delqhi-sin-github-issues.hf.space`
|
|
|
|
| 43 |
- best-effort `sin-supabase` audit persistence for workflow events
|
| 44 |
- canonical env names: `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` (legacy `SIN_SUPABASE_*` still accepted)
|
| 45 |
|
| 46 |
+
## GitHub App Fleet Routing
|
| 47 |
+
|
| 48 |
+
- Each specialized SIN coder can own its own dedicated GitHub App identity, for example `SIN-Hermes[bot]`, `SIN-Zeus[bot]`, or `SIN-BugBounty[bot]`.
|
| 49 |
+
- Central webhook endpoint: `POST /api/webhooks/github-apps`
|
| 50 |
+
- The current implementation prefers one central endpoint with `installation.app_id` routing.
|
| 51 |
+
- Routing key: `installation.app_id`
|
| 52 |
+
- Reply auth key: `installation.id`
|
| 53 |
+
- Routing config sources:
|
| 54 |
+
- repo SSOT: `config/github-app-routing.json`
|
| 55 |
+
- start-small example: `config/github-app-routing.local.example.json`
|
| 56 |
+
- GitHub-Issues local example: `config/github-app-routing.example.json`
|
| 57 |
+
- env matrix: `.env.example` and `.env.example` in this agent root
|
| 58 |
+
- Secret material is never committed. Use env-backed private key + webhook secret fields referenced from the routing config.
|
| 59 |
+
|
| 60 |
+
### Operator Setup
|
| 61 |
+
|
| 62 |
+
1. Generate a local routing file from the example template:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
npm run ops:github-app-routing:init
|
| 66 |
+
npm run ops:github-app-routing:env-template -- --agents sin-hermes,sin-zeus,sin-bugbounty
|
| 67 |
+
npm run ops:github-app-routing:vm-rollout -- --agents sin-hermes,sin-zeus,sin-bugbounty --base-url https://your-vm-host
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
2. Export `SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json`.
|
| 71 |
+
3. Fill the referenced env vars for each GitHub App in your runtime environment:
|
| 72 |
+
- `SIN_HERMES_GITHUB_APP_ID`, `SIN_HERMES_GITHUB_APP_CLIENT_ID`, `SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET`
|
| 73 |
+
- `SIN_ZEUS_GITHUB_APP_ID`, `SIN_ZEUS_GITHUB_APP_CLIENT_ID`, `SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET`
|
| 74 |
+
- `SIN_GITHUB_ISSUES_GITHUB_APP_ID`, `SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID`, `SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET`
|
| 75 |
+
4. Point the selected GitHub Apps to the shared webhook path `POST /api/webhooks/github-apps`.
|
| 76 |
+
5. Validate the webhook router before going live:
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
npm run ops:github-app-routing:check -- --apps sin-hermes,sin-zeus,sin-github-issues
|
| 80 |
+
npm run ops:github-app-routing:smoke -- --app sin-hermes --base-url http://127.0.0.1:8000
|
| 81 |
+
npm run ops:github-app-routing:smoke -- --app sin-zeus --base-url http://127.0.0.1:8000
|
| 82 |
+
npm run ops:github-app-routing:smoke -- --app sin-github-issues --base-url http://127.0.0.1:8000
|
| 83 |
+
pytest tests/unit/test_room13_webhook_routes.py -q
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
The smoke command generates a signed synthetic GitHub webhook for the selected app and expects the Room-13 endpoint to resolve the configured `app_slug`, `preferred_agent_slug`, and `installation_id`.
|
| 87 |
+
|
| 88 |
## A2A Surface
|
| 89 |
|
| 90 |
- Card: `GET /.well-known/agent-card.json`
|
|
|
|
| 116 |
sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
|
| 117 |
sin-github-issues run-action '{"action":"sin.github.issues.issue.evidence.policy"}'
|
| 118 |
sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
|
| 119 |
+
sin-github-issues run-action '{"action":"sin.github.issues.runners.list","repo":"OpenSIN-AI/OpenSIN-backend"}'
|
| 120 |
+
sin-github-issues run-action '{"action":"sin.github.issues.workflow.runs.list","repo":"OpenSIN-AI/OpenSIN-backend","branch":"sin-github-issues-bootstrap"}'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
sin-github-issues run-action '{"action":"sin.github.issues.workflow.local_suite.run","repoPath":"/abs/repo","suite":"docs-and-architecture","confirm":true}'
|
| 122 |
```
|
| 123 |
|
agent.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
{
|
| 2 |
"id": "sin-github-issues",
|
| 3 |
-
"displayName": "
|
| 4 |
"team": "team-coding",
|
| 5 |
-
"description": "Elite 2026 GitHub
|
| 6 |
"version": "2026.03.22",
|
| 7 |
"deployment": {
|
| 8 |
"workforceIndex": "https://a2a.delqhi.com",
|
|
@@ -16,6 +16,7 @@
|
|
| 16 |
"rpcPath": "/a2a/v1",
|
| 17 |
"transport": "jsonrpc-over-http"
|
| 18 |
},
|
|
|
|
| 19 |
"mcp": {
|
| 20 |
"transport": "stdio",
|
| 21 |
"configPath": "./mcp-config.json"
|
|
|
|
| 1 |
{
|
| 2 |
"id": "sin-github-issues",
|
| 3 |
+
"displayName": "SIN-GitHub-Issues",
|
| 4 |
"team": "team-coding",
|
| 5 |
+
"description": "Elite 2026 GitHub Operations Architect. Manages the entire project lifecycle via GitHub Projects, Issues, Wikis, Discussions, Security Alerts (Dependabot/CodeQL), PR Reviews, and Gists. Fully integrated with openai/gpt-5.4 and the A2A Token Pool.",
|
| 6 |
"version": "2026.03.22",
|
| 7 |
"deployment": {
|
| 8 |
"workforceIndex": "https://a2a.delqhi.com",
|
|
|
|
| 16 |
"rpcPath": "/a2a/v1",
|
| 17 |
"transport": "jsonrpc-over-http"
|
| 18 |
},
|
| 19 |
+
"primaryModel": "openai/gpt-5.4",
|
| 20 |
"mcp": {
|
| 21 |
"transport": "stdio",
|
| 22 |
"configPath": "./mcp-config.json"
|
config/github-app-routing.example.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mode": "central-endpoint-routing",
|
| 3 |
+
"defaultWebhookPath": "/api/webhooks/github-apps",
|
| 4 |
+
"apps": [
|
| 5 |
+
{
|
| 6 |
+
"appIdEnv": "SIN_HERMES_GITHUB_APP_ID",
|
| 7 |
+
"appSlug": "sin-hermes",
|
| 8 |
+
"botName": "SIN-Hermes[bot]",
|
| 9 |
+
"preferredAgentSlug": "sin-hermesbote",
|
| 10 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 11 |
+
"clientIdEnv": "SIN_HERMES_GITHUB_APP_CLIENT_ID",
|
| 12 |
+
"privateKeyEnv": "SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 13 |
+
"webhookSecretEnv": "SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"appIdEnv": "SIN_ZEUS_GITHUB_APP_ID",
|
| 17 |
+
"appSlug": "sin-zeus",
|
| 18 |
+
"botName": "SIN-Zeus[bot]",
|
| 19 |
+
"preferredAgentSlug": "sin-coding-ceo",
|
| 20 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 21 |
+
"clientIdEnv": "SIN_ZEUS_GITHUB_APP_CLIENT_ID",
|
| 22 |
+
"privateKeyEnv": "SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 23 |
+
"webhookSecretEnv": "SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"appIdEnv": "SIN_BUGBOUNTY_GITHUB_APP_ID",
|
| 27 |
+
"appSlug": "sin-bugbounty",
|
| 28 |
+
"botName": "SIN-BugBounty[bot]",
|
| 29 |
+
"preferredAgentSlug": "sin-bugbounty",
|
| 30 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 31 |
+
"clientIdEnv": "SIN_BUGBOUNTY_GITHUB_APP_CLIENT_ID",
|
| 32 |
+
"privateKeyEnv": "SIN_BUGBOUNTY_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 33 |
+
"webhookSecretEnv": "SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"appIdEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_ID",
|
| 37 |
+
"appSlug": "sin-github-issues",
|
| 38 |
+
"botName": "SIN-GitHub-Issues[bot]",
|
| 39 |
+
"preferredAgentSlug": "sin-github-issues",
|
| 40 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 41 |
+
"clientIdEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID",
|
| 42 |
+
"privateKeyEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 43 |
+
"webhookSecretEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"appIdEnv": "SIN_BACKEND_GITHUB_APP_ID",
|
| 47 |
+
"appSlug": "sin-backend",
|
| 48 |
+
"botName": "SIN-Backend[bot]",
|
| 49 |
+
"preferredAgentSlug": "sin-backend",
|
| 50 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 51 |
+
"clientIdEnv": "SIN_BACKEND_GITHUB_APP_CLIENT_ID",
|
| 52 |
+
"privateKeyEnv": "SIN_BACKEND_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 53 |
+
"webhookSecretEnv": "SIN_BACKEND_GITHUB_APP_WEBHOOK_SECRET"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"appIdEnv": "SIN_FRONTEND_GITHUB_APP_ID",
|
| 57 |
+
"appSlug": "sin-frontend",
|
| 58 |
+
"botName": "SIN-Frontend[bot]",
|
| 59 |
+
"preferredAgentSlug": "sin-frontend",
|
| 60 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 61 |
+
"clientIdEnv": "SIN_FRONTEND_GITHUB_APP_CLIENT_ID",
|
| 62 |
+
"privateKeyEnv": "SIN_FRONTEND_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 63 |
+
"webhookSecretEnv": "SIN_FRONTEND_GITHUB_APP_WEBHOOK_SECRET"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"appIdEnv": "SIN_TESTER_GITHUB_APP_ID",
|
| 67 |
+
"appSlug": "sin-tester",
|
| 68 |
+
"botName": "SIN-Tester[bot]",
|
| 69 |
+
"preferredAgentSlug": "sin-tester",
|
| 70 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 71 |
+
"clientIdEnv": "SIN_TESTER_GITHUB_APP_CLIENT_ID",
|
| 72 |
+
"privateKeyEnv": "SIN_TESTER_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 73 |
+
"webhookSecretEnv": "SIN_TESTER_GITHUB_APP_WEBHOOK_SECRET"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"appIdEnv": "SIN_CODE_DATABASE_GITHUB_APP_ID",
|
| 77 |
+
"appSlug": "sin-code-database",
|
| 78 |
+
"botName": "SIN-Code-Database[bot]",
|
| 79 |
+
"preferredAgentSlug": "sin-code-database",
|
| 80 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 81 |
+
"clientIdEnv": "SIN_CODE_DATABASE_GITHUB_APP_CLIENT_ID",
|
| 82 |
+
"privateKeyEnv": "SIN_CODE_DATABASE_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 83 |
+
"webhookSecretEnv": "SIN_CODE_DATABASE_GITHUB_APP_WEBHOOK_SECRET"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"appIdEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_ID",
|
| 87 |
+
"appSlug": "sin-code-integration",
|
| 88 |
+
"botName": "SIN-Code-Integration[bot]",
|
| 89 |
+
"preferredAgentSlug": "sin-code-integration",
|
| 90 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 91 |
+
"clientIdEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_CLIENT_ID",
|
| 92 |
+
"privateKeyEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 93 |
+
"webhookSecretEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_WEBHOOK_SECRET"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"appIdEnv": "SIN_CODE_AI_GITHUB_APP_ID",
|
| 97 |
+
"appSlug": "sin-code-ai",
|
| 98 |
+
"botName": "SIN-Code-AI[bot]",
|
| 99 |
+
"preferredAgentSlug": "sin-code-ai",
|
| 100 |
+
"webhookPath": "/api/webhooks/github-apps",
|
| 101 |
+
"clientIdEnv": "SIN_CODE_AI_GITHUB_APP_CLIENT_ID",
|
| 102 |
+
"privateKeyEnv": "SIN_CODE_AI_GITHUB_APP_PRIVATE_KEY_PEM",
|
| 103 |
+
"webhookSecretEnv": "SIN_CODE_AI_GITHUB_APP_WEBHOOK_SECRET"
|
| 104 |
+
}
|
| 105 |
+
]
|
| 106 |
+
}
|
package-lock.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "@
|
| 3 |
"version": "0.1.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
-
"name": "@
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@huggingface/hub": "^0.15.1",
|
| 12 |
"@modelcontextprotocol/sdk": "latest",
|
| 13 |
"@supabase/supabase-js": "^2.39.7",
|
|
|
|
| 14 |
"node-cron": "^3.0.3",
|
| 15 |
"tar": "^6.2.0",
|
| 16 |
"zod": "^3.25.76"
|
|
@@ -19,6 +20,7 @@
|
|
| 19 |
"sin-github-issues": "dist/src/cli.js"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
|
|
|
| 22 |
"@types/node": "^24.3.0",
|
| 23 |
"@types/node-cron": "^3.0.11",
|
| 24 |
"@types/tar": "^6.1.11",
|
|
@@ -181,6 +183,24 @@
|
|
| 181 |
"node": ">=20.0.0"
|
| 182 |
}
|
| 183 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
"node_modules/@types/node": {
|
| 185 |
"version": "24.12.0",
|
| 186 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
|
@@ -287,6 +307,12 @@
|
|
| 287 |
"url": "https://opencollective.com/express"
|
| 288 |
}
|
| 289 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
"node_modules/bytes": {
|
| 291 |
"version": "3.1.2",
|
| 292 |
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
|
@@ -445,6 +471,15 @@
|
|
| 445 |
"node": ">= 0.4"
|
| 446 |
}
|
| 447 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
"node_modules/ee-first": {
|
| 449 |
"version": "1.1.1",
|
| 450 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
@@ -865,6 +900,91 @@
|
|
| 865 |
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
| 866 |
"license": "BSD-2-Clause"
|
| 867 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
"node_modules/math-intrinsics": {
|
| 869 |
"version": "1.1.0",
|
| 870 |
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
|
@@ -1150,12 +1270,44 @@
|
|
| 1150 |
"node": ">= 18"
|
| 1151 |
}
|
| 1152 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
"node_modules/safer-buffer": {
|
| 1154 |
"version": "2.1.2",
|
| 1155 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 1156 |
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 1157 |
"license": "MIT"
|
| 1158 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
"node_modules/send": {
|
| 1160 |
"version": "1.2.1",
|
| 1161 |
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "@opensin-backend/sin-github-issues",
|
| 3 |
"version": "0.1.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
+
"name": "@opensin-backend/sin-github-issues",
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@huggingface/hub": "^0.15.1",
|
| 12 |
"@modelcontextprotocol/sdk": "latest",
|
| 13 |
"@supabase/supabase-js": "^2.39.7",
|
| 14 |
+
"jsonwebtoken": "^9.0.2",
|
| 15 |
"node-cron": "^3.0.3",
|
| 16 |
"tar": "^6.2.0",
|
| 17 |
"zod": "^3.25.76"
|
|
|
|
| 20 |
"sin-github-issues": "dist/src/cli.js"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
| 23 |
+
"@types/jsonwebtoken": "^9.0.10",
|
| 24 |
"@types/node": "^24.3.0",
|
| 25 |
"@types/node-cron": "^3.0.11",
|
| 26 |
"@types/tar": "^6.1.11",
|
|
|
|
| 183 |
"node": ">=20.0.0"
|
| 184 |
}
|
| 185 |
},
|
| 186 |
+
"node_modules/@types/jsonwebtoken": {
|
| 187 |
+
"version": "9.0.10",
|
| 188 |
+
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
| 189 |
+
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
| 190 |
+
"dev": true,
|
| 191 |
+
"license": "MIT",
|
| 192 |
+
"dependencies": {
|
| 193 |
+
"@types/ms": "*",
|
| 194 |
+
"@types/node": "*"
|
| 195 |
+
}
|
| 196 |
+
},
|
| 197 |
+
"node_modules/@types/ms": {
|
| 198 |
+
"version": "2.1.0",
|
| 199 |
+
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
| 200 |
+
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
| 201 |
+
"dev": true,
|
| 202 |
+
"license": "MIT"
|
| 203 |
+
},
|
| 204 |
"node_modules/@types/node": {
|
| 205 |
"version": "24.12.0",
|
| 206 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
|
|
|
| 307 |
"url": "https://opencollective.com/express"
|
| 308 |
}
|
| 309 |
},
|
| 310 |
+
"node_modules/buffer-equal-constant-time": {
|
| 311 |
+
"version": "1.0.1",
|
| 312 |
+
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
| 313 |
+
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
| 314 |
+
"license": "BSD-3-Clause"
|
| 315 |
+
},
|
| 316 |
"node_modules/bytes": {
|
| 317 |
"version": "3.1.2",
|
| 318 |
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
|
|
|
| 471 |
"node": ">= 0.4"
|
| 472 |
}
|
| 473 |
},
|
| 474 |
+
"node_modules/ecdsa-sig-formatter": {
|
| 475 |
+
"version": "1.0.11",
|
| 476 |
+
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
| 477 |
+
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
| 478 |
+
"license": "Apache-2.0",
|
| 479 |
+
"dependencies": {
|
| 480 |
+
"safe-buffer": "^5.0.1"
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
"node_modules/ee-first": {
|
| 484 |
"version": "1.1.1",
|
| 485 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
|
|
| 900 |
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
| 901 |
"license": "BSD-2-Clause"
|
| 902 |
},
|
| 903 |
+
"node_modules/jsonwebtoken": {
|
| 904 |
+
"version": "9.0.3",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
| 906 |
+
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
| 907 |
+
"license": "MIT",
|
| 908 |
+
"dependencies": {
|
| 909 |
+
"jws": "^4.0.1",
|
| 910 |
+
"lodash.includes": "^4.3.0",
|
| 911 |
+
"lodash.isboolean": "^3.0.3",
|
| 912 |
+
"lodash.isinteger": "^4.0.4",
|
| 913 |
+
"lodash.isnumber": "^3.0.3",
|
| 914 |
+
"lodash.isplainobject": "^4.0.6",
|
| 915 |
+
"lodash.isstring": "^4.0.1",
|
| 916 |
+
"lodash.once": "^4.0.0",
|
| 917 |
+
"ms": "^2.1.1",
|
| 918 |
+
"semver": "^7.5.4"
|
| 919 |
+
},
|
| 920 |
+
"engines": {
|
| 921 |
+
"node": ">=12",
|
| 922 |
+
"npm": ">=6"
|
| 923 |
+
}
|
| 924 |
+
},
|
| 925 |
+
"node_modules/jwa": {
|
| 926 |
+
"version": "2.0.1",
|
| 927 |
+
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
| 928 |
+
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
| 929 |
+
"license": "MIT",
|
| 930 |
+
"dependencies": {
|
| 931 |
+
"buffer-equal-constant-time": "^1.0.1",
|
| 932 |
+
"ecdsa-sig-formatter": "1.0.11",
|
| 933 |
+
"safe-buffer": "^5.0.1"
|
| 934 |
+
}
|
| 935 |
+
},
|
| 936 |
+
"node_modules/jws": {
|
| 937 |
+
"version": "4.0.1",
|
| 938 |
+
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
| 939 |
+
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
| 940 |
+
"license": "MIT",
|
| 941 |
+
"dependencies": {
|
| 942 |
+
"jwa": "^2.0.1",
|
| 943 |
+
"safe-buffer": "^5.0.1"
|
| 944 |
+
}
|
| 945 |
+
},
|
| 946 |
+
"node_modules/lodash.includes": {
|
| 947 |
+
"version": "4.3.0",
|
| 948 |
+
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
| 949 |
+
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
| 950 |
+
"license": "MIT"
|
| 951 |
+
},
|
| 952 |
+
"node_modules/lodash.isboolean": {
|
| 953 |
+
"version": "3.0.3",
|
| 954 |
+
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
| 955 |
+
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
| 956 |
+
"license": "MIT"
|
| 957 |
+
},
|
| 958 |
+
"node_modules/lodash.isinteger": {
|
| 959 |
+
"version": "4.0.4",
|
| 960 |
+
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
| 961 |
+
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
| 962 |
+
"license": "MIT"
|
| 963 |
+
},
|
| 964 |
+
"node_modules/lodash.isnumber": {
|
| 965 |
+
"version": "3.0.3",
|
| 966 |
+
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
| 967 |
+
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
| 968 |
+
"license": "MIT"
|
| 969 |
+
},
|
| 970 |
+
"node_modules/lodash.isplainobject": {
|
| 971 |
+
"version": "4.0.6",
|
| 972 |
+
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
| 973 |
+
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
| 974 |
+
"license": "MIT"
|
| 975 |
+
},
|
| 976 |
+
"node_modules/lodash.isstring": {
|
| 977 |
+
"version": "4.0.1",
|
| 978 |
+
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
| 979 |
+
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
| 980 |
+
"license": "MIT"
|
| 981 |
+
},
|
| 982 |
+
"node_modules/lodash.once": {
|
| 983 |
+
"version": "4.1.1",
|
| 984 |
+
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
| 985 |
+
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
| 986 |
+
"license": "MIT"
|
| 987 |
+
},
|
| 988 |
"node_modules/math-intrinsics": {
|
| 989 |
"version": "1.1.0",
|
| 990 |
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
|
|
|
| 1270 |
"node": ">= 18"
|
| 1271 |
}
|
| 1272 |
},
|
| 1273 |
+
"node_modules/safe-buffer": {
|
| 1274 |
+
"version": "5.2.1",
|
| 1275 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 1276 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 1277 |
+
"funding": [
|
| 1278 |
+
{
|
| 1279 |
+
"type": "github",
|
| 1280 |
+
"url": "https://github.com/sponsors/feross"
|
| 1281 |
+
},
|
| 1282 |
+
{
|
| 1283 |
+
"type": "patreon",
|
| 1284 |
+
"url": "https://www.patreon.com/feross"
|
| 1285 |
+
},
|
| 1286 |
+
{
|
| 1287 |
+
"type": "consulting",
|
| 1288 |
+
"url": "https://feross.org/support"
|
| 1289 |
+
}
|
| 1290 |
+
],
|
| 1291 |
+
"license": "MIT"
|
| 1292 |
+
},
|
| 1293 |
"node_modules/safer-buffer": {
|
| 1294 |
"version": "2.1.2",
|
| 1295 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 1296 |
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 1297 |
"license": "MIT"
|
| 1298 |
},
|
| 1299 |
+
"node_modules/semver": {
|
| 1300 |
+
"version": "7.7.4",
|
| 1301 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
| 1302 |
+
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
| 1303 |
+
"license": "ISC",
|
| 1304 |
+
"bin": {
|
| 1305 |
+
"semver": "bin/semver.js"
|
| 1306 |
+
},
|
| 1307 |
+
"engines": {
|
| 1308 |
+
"node": ">=10"
|
| 1309 |
+
}
|
| 1310 |
+
},
|
| 1311 |
"node_modules/send": {
|
| 1312 |
"version": "1.2.1",
|
| 1313 |
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "@
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"type": "module",
|
|
@@ -10,8 +10,14 @@
|
|
| 10 |
},
|
| 11 |
"scripts": {
|
| 12 |
"build": "rm -rf dist && tsc -p tsconfig.json && node scripts/postbuild.mjs",
|
| 13 |
-
"
|
| 14 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"typecheck": "tsc --noEmit -p tsconfig.json",
|
| 16 |
"start:a2a": "node dist/src/cli.js serve-a2a",
|
| 17 |
"start:mcp": "node dist/src/cli.js serve-mcp"
|
|
@@ -20,11 +26,13 @@
|
|
| 20 |
"@modelcontextprotocol/sdk": "latest",
|
| 21 |
"zod": "^3.25.76",
|
| 22 |
"@huggingface/hub": "^0.15.1",
|
|
|
|
| 23 |
"node-cron": "^3.0.3",
|
| 24 |
"tar": "^6.2.0",
|
| 25 |
"@supabase/supabase-js": "^2.39.7"
|
| 26 |
},
|
| 27 |
"devDependencies": {
|
|
|
|
| 28 |
"@types/node": "^24.3.0",
|
| 29 |
"typescript": "^5.9.3",
|
| 30 |
"@types/node-cron": "^3.0.11",
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "@opensin-backend/sin-github-issues",
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"type": "module",
|
|
|
|
| 10 |
},
|
| 11 |
"scripts": {
|
| 12 |
"build": "rm -rf dist && tsc -p tsconfig.json && node scripts/postbuild.mjs",
|
| 13 |
+
"ops:hf-deploy:check": "node scripts/check-hf-deploy-prereqs.mjs",
|
| 14 |
+
"ops:github-app-routing:env-template": "node scripts/render-github-app-env-template.mjs",
|
| 15 |
+
"ops:github-app-routing:init": "node scripts/init-github-app-routing-local.mjs",
|
| 16 |
+
"ops:github-app-routing:vm-rollout": "node scripts/render-github-app-vm-rollout.mjs",
|
| 17 |
+
"ops:github-app-comment": "node scripts/comment-as-github-app.mjs",
|
| 18 |
+
"ops:github-app-routing:check": "node scripts/check-github-app-routing.mjs",
|
| 19 |
+
"ops:github-app-routing:smoke": "node scripts/smoke-github-app-routing.mjs",
|
| 20 |
+
"test": "npm run build && node --test test/*.mjs",
|
| 21 |
"typecheck": "tsc --noEmit -p tsconfig.json",
|
| 22 |
"start:a2a": "node dist/src/cli.js serve-a2a",
|
| 23 |
"start:mcp": "node dist/src/cli.js serve-mcp"
|
|
|
|
| 26 |
"@modelcontextprotocol/sdk": "latest",
|
| 27 |
"zod": "^3.25.76",
|
| 28 |
"@huggingface/hub": "^0.15.1",
|
| 29 |
+
"jsonwebtoken": "^9.0.2",
|
| 30 |
"node-cron": "^3.0.3",
|
| 31 |
"tar": "^6.2.0",
|
| 32 |
"@supabase/supabase-js": "^2.39.7"
|
| 33 |
},
|
| 34 |
"devDependencies": {
|
| 35 |
+
"@types/jsonwebtoken": "^9.0.10",
|
| 36 |
"@types/node": "^24.3.0",
|
| 37 |
"typescript": "^5.9.3",
|
| 38 |
"@types/node-cron": "^3.0.11",
|
scripts/check-github-app-routing.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { readFile } from 'node:fs/promises';
|
| 4 |
+
import path from 'node:path';
|
| 5 |
+
|
| 6 |
+
const cwd = process.cwd();
|
| 7 |
+
const args = process.argv.slice(2);
|
| 8 |
+
|
| 9 |
+
function parseArgs(argv) {
|
| 10 |
+
const out = {};
|
| 11 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 12 |
+
const token = argv[i];
|
| 13 |
+
if (!token.startsWith('--')) continue;
|
| 14 |
+
const key = token.slice(2);
|
| 15 |
+
const next = argv[i + 1];
|
| 16 |
+
if (!next || next.startsWith('--')) {
|
| 17 |
+
out[key] = 'true';
|
| 18 |
+
continue;
|
| 19 |
+
}
|
| 20 |
+
out[key] = next;
|
| 21 |
+
i += 1;
|
| 22 |
+
}
|
| 23 |
+
return out;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const parsedArgs = parseArgs(args);
|
| 27 |
+
const configPath = path.resolve(
|
| 28 |
+
cwd,
|
| 29 |
+
parsedArgs['config'] || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json',
|
| 30 |
+
);
|
| 31 |
+
|
| 32 |
+
function firstString(...values) {
|
| 33 |
+
for (const value of values) {
|
| 34 |
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
| 35 |
+
}
|
| 36 |
+
return null;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function readEnv(name) {
|
| 40 |
+
if (!name) return null;
|
| 41 |
+
const value = process.env[name];
|
| 42 |
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function parseList(value) {
|
| 46 |
+
if (!value || typeof value !== 'string') return [];
|
| 47 |
+
return value
|
| 48 |
+
.split(',')
|
| 49 |
+
.map((item) => item.trim())
|
| 50 |
+
.filter(Boolean);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function toRoute(entry, defaultWebhookPath) {
|
| 54 |
+
const appId = firstString(String(entry.appId || '').trim(), readEnv(entry.appIdEnv));
|
| 55 |
+
const clientId = firstString(entry.clientId, readEnv(entry.clientIdEnv));
|
| 56 |
+
const routePath = firstString(entry.routePath, entry.webhookPath, defaultWebhookPath || '/github/webhook');
|
| 57 |
+
const agentSlug = firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug);
|
| 58 |
+
const privateKey = firstString(entry.privateKeyPem, readEnv(entry.privateKeyEnv));
|
| 59 |
+
const webhookSecret = firstString(entry.webhookSecret, readEnv(entry.webhookSecretEnv));
|
| 60 |
+
return {
|
| 61 |
+
agentSlug,
|
| 62 |
+
appSlug: firstString(entry.appSlug),
|
| 63 |
+
botName: firstString(entry.botName),
|
| 64 |
+
routePath,
|
| 65 |
+
appId,
|
| 66 |
+
clientId,
|
| 67 |
+
privateKeyRef: firstString(entry.privateKeyEnv, entry.privateKeyPath, entry.privateKeyPem ? 'inline' : null),
|
| 68 |
+
webhookSecretRef: firstString(entry.webhookSecretEnv, entry.webhookSecretPath, entry.webhookSecret ? 'inline' : null),
|
| 69 |
+
privateKeyResolved: Boolean(privateKey),
|
| 70 |
+
webhookSecretResolved: Boolean(webhookSecret),
|
| 71 |
+
};
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async function main() {
|
| 75 |
+
const raw = await readFile(configPath, 'utf8');
|
| 76 |
+
const config = JSON.parse(raw);
|
| 77 |
+
const defaultWebhookPath = firstString(config.defaultWebhookPath, '/github/webhook');
|
| 78 |
+
const agentsFilter = new Set(parseList(parsedArgs.agents || parsedArgs.agent));
|
| 79 |
+
const configuredOnly = parsedArgs['configured-only'] === 'true';
|
| 80 |
+
const apps = (Array.isArray(config.apps) ? config.apps.map((entry) => toRoute(entry, defaultWebhookPath)) : []).filter((app) => {
|
| 81 |
+
const matchesAgent = agentsFilter.size === 0 || agentsFilter.has(app.agentSlug || '') || agentsFilter.has(app.appSlug || '');
|
| 82 |
+
const isConfigured = !configuredOnly || Boolean(app.appId || app.clientId || app.privateKeyResolved || app.webhookSecretResolved);
|
| 83 |
+
return matchesAgent && isConfigured;
|
| 84 |
+
});
|
| 85 |
+
const missing = apps.flatMap((app) => {
|
| 86 |
+
const failures = [];
|
| 87 |
+
if (!app.agentSlug) failures.push('agentSlug');
|
| 88 |
+
if (!app.appId) failures.push('appId');
|
| 89 |
+
if (!app.clientId) failures.push('clientId');
|
| 90 |
+
if (!app.privateKeyResolved) failures.push('privateKey');
|
| 91 |
+
if (!app.webhookSecretResolved) failures.push('webhookSecret');
|
| 92 |
+
return failures.length ? [{ agentSlug: app.agentSlug || app.appSlug || 'unknown', missing: failures }] : [];
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
console.log(JSON.stringify({
|
| 96 |
+
ok: missing.length === 0,
|
| 97 |
+
configPath,
|
| 98 |
+
defaultWebhookPath,
|
| 99 |
+
filters: {
|
| 100 |
+
agents: [...agentsFilter],
|
| 101 |
+
configuredOnly,
|
| 102 |
+
},
|
| 103 |
+
apps,
|
| 104 |
+
missing,
|
| 105 |
+
}, null, 2));
|
| 106 |
+
|
| 107 |
+
if (missing.length > 0) process.exitCode = 1;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
main().catch((error) => {
|
| 111 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 112 |
+
process.exit(1);
|
| 113 |
+
});
|
scripts/check-hf-deploy-prereqs.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { execFile } from 'node:child_process';
|
| 4 |
+
import { promisify } from 'node:util';
|
| 5 |
+
|
| 6 |
+
const execFileAsync = promisify(execFile);
|
| 7 |
+
|
| 8 |
+
function parseArgs(argv) {
|
| 9 |
+
const out = {};
|
| 10 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 11 |
+
const token = argv[i];
|
| 12 |
+
if (!token.startsWith('--')) continue;
|
| 13 |
+
const key = token.slice(2);
|
| 14 |
+
const next = argv[i + 1];
|
| 15 |
+
if (!next || next.startsWith('--')) {
|
| 16 |
+
out[key] = 'true';
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
out[key] = next;
|
| 20 |
+
i += 1;
|
| 21 |
+
}
|
| 22 |
+
return out;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
async function safeGh(args) {
|
| 26 |
+
try {
|
| 27 |
+
const { stdout } = await execFileAsync('gh', args, { cwd: process.cwd() });
|
| 28 |
+
return { ok: true, stdout };
|
| 29 |
+
} catch (error) {
|
| 30 |
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
async function main() {
|
| 35 |
+
const args = parseArgs(process.argv.slice(2));
|
| 36 |
+
const repo = args.repo || 'Delqhi/sin-github-issues';
|
| 37 |
+
const workflow = args.workflow || '.github/workflows/deploy-huggingface-space.yml';
|
| 38 |
+
|
| 39 |
+
const repoMeta = await safeGh(['repo', 'view', repo, '--json', 'name,isPrivate,url,defaultBranchRef']);
|
| 40 |
+
const secrets = await safeGh(['secret', 'list', '-R', repo, '--json', 'name,updatedAt,visibility']);
|
| 41 |
+
const workflows = await safeGh(['workflow', 'list', '-R', repo, '--json', 'name,path,state,id']);
|
| 42 |
+
const runs = await safeGh(['run', 'list', '-R', repo, '--workflow', workflow, '--limit', '3', '--json', 'databaseId,status,conclusion,headSha,createdAt,name,workflowName,url,jobs']);
|
| 43 |
+
|
| 44 |
+
const parsedSecrets = secrets.ok ? JSON.parse(secrets.stdout || '[]') : [];
|
| 45 |
+
const parsedWorkflows = workflows.ok ? JSON.parse(workflows.stdout || '[]') : [];
|
| 46 |
+
const parsedRuns = runs.ok ? JSON.parse(runs.stdout || '[]') : [];
|
| 47 |
+
const hasHfTokenSecret = parsedSecrets.some((entry) => entry.name === 'HF_TOKEN');
|
| 48 |
+
const deployWorkflow = parsedWorkflows.find((entry) => entry.path === workflow || entry.name === workflow || entry.name === 'Deploy Hugging Face Space');
|
| 49 |
+
const latestRun = parsedRuns[0] || null;
|
| 50 |
+
|
| 51 |
+
const failures = [];
|
| 52 |
+
if (!repoMeta.ok) failures.push('repo_view_failed');
|
| 53 |
+
if (!secrets.ok) failures.push('secret_list_failed');
|
| 54 |
+
if (!workflows.ok) failures.push('workflow_list_failed');
|
| 55 |
+
if (!deployWorkflow) failures.push('deploy_workflow_missing');
|
| 56 |
+
if (!hasHfTokenSecret) failures.push('hf_token_secret_missing');
|
| 57 |
+
if (latestRun && latestRun.conclusion === 'failure' && Array.isArray(latestRun.jobs) && latestRun.jobs.length === 0) {
|
| 58 |
+
failures.push('latest_deploy_run_failed_without_jobs');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const output = {
|
| 62 |
+
ok: failures.length === 0,
|
| 63 |
+
repo,
|
| 64 |
+
workflow,
|
| 65 |
+
repoMeta: repoMeta.ok ? JSON.parse(repoMeta.stdout) : { error: repoMeta.error },
|
| 66 |
+
hasHfTokenSecret,
|
| 67 |
+
deployWorkflow: deployWorkflow || null,
|
| 68 |
+
latestRun,
|
| 69 |
+
failures,
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
console.log(JSON.stringify(output, null, 2));
|
| 73 |
+
if (failures.length > 0) process.exitCode = 1;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
main().catch((error) => {
|
| 77 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 78 |
+
process.exit(1);
|
| 79 |
+
});
|
scripts/comment-as-github-app.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { execFile } from 'node:child_process';
|
| 4 |
+
import { promisify } from 'node:util';
|
| 5 |
+
|
| 6 |
+
const execFileAsync = promisify(execFile);
|
| 7 |
+
|
| 8 |
+
function parseArgs(argv) {
|
| 9 |
+
const out = {};
|
| 10 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 11 |
+
const token = argv[i];
|
| 12 |
+
if (!token.startsWith('--')) continue;
|
| 13 |
+
const key = token.slice(2);
|
| 14 |
+
const next = argv[i + 1];
|
| 15 |
+
if (!next || next.startsWith('--')) {
|
| 16 |
+
out[key] = 'true';
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
out[key] = next;
|
| 20 |
+
i += 1;
|
| 21 |
+
}
|
| 22 |
+
return out;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function required(args, key) {
|
| 26 |
+
const value = args[key];
|
| 27 |
+
if (!value || !String(value).trim()) {
|
| 28 |
+
throw new Error(`missing_required_arg: --${key}`);
|
| 29 |
+
}
|
| 30 |
+
return String(value).trim();
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
async function main() {
|
| 34 |
+
const args = parseArgs(process.argv.slice(2));
|
| 35 |
+
const action = {
|
| 36 |
+
action: 'sin.github.issue.comment.as_app',
|
| 37 |
+
agentSlug: required(args, 'agent'),
|
| 38 |
+
repo: required(args, 'repo'),
|
| 39 |
+
issueNumber: Number(required(args, 'issue-number')),
|
| 40 |
+
installationId: Number(required(args, 'installation-id')),
|
| 41 |
+
body: required(args, 'body'),
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
if (!Number.isFinite(action.issueNumber) || !Number.isFinite(action.installationId)) {
|
| 45 |
+
throw new Error('invalid_numeric_args: --issue-number and --installation-id must be numbers');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const { stdout } = await execFileAsync('node', ['dist/src/cli.js', 'run-action', JSON.stringify(action)], {
|
| 49 |
+
cwd: process.cwd(),
|
| 50 |
+
});
|
| 51 |
+
process.stdout.write(stdout);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
main().catch((error) => {
|
| 55 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 56 |
+
process.exit(1);
|
| 57 |
+
});
|
scripts/hf_pull_script.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
|
|
|
| 3 |
Pulls a fresh OpenAI token from SIN-Supabase when a rate-limit is hit.
|
| 4 |
Infinite Scaling Architecture.
|
| 5 |
"""
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
A2A Consumer Rotator (HF VM Pull Script)
|
| 4 |
Pulls a fresh OpenAI token from SIN-Supabase when a rate-limit is hit.
|
| 5 |
Infinite Scaling Architecture.
|
| 6 |
"""
|
scripts/init-github-app-routing-local.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { access, copyFile, mkdir } from 'node:fs/promises';
|
| 4 |
+
import path from 'node:path';
|
| 5 |
+
|
| 6 |
+
const cwd = process.cwd();
|
| 7 |
+
|
| 8 |
+
function parseArgs(argv) {
|
| 9 |
+
const out = {};
|
| 10 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 11 |
+
const token = argv[i];
|
| 12 |
+
if (!token.startsWith('--')) continue;
|
| 13 |
+
const key = token.slice(2);
|
| 14 |
+
const next = argv[i + 1];
|
| 15 |
+
if (!next || next.startsWith('--')) {
|
| 16 |
+
out[key] = 'true';
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
out[key] = next;
|
| 20 |
+
i += 1;
|
| 21 |
+
}
|
| 22 |
+
return out;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
async function exists(filePath) {
|
| 26 |
+
try {
|
| 27 |
+
await access(filePath);
|
| 28 |
+
return true;
|
| 29 |
+
} catch {
|
| 30 |
+
return false;
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
async function main() {
|
| 35 |
+
const args = parseArgs(process.argv.slice(2));
|
| 36 |
+
const configDir = path.resolve(cwd, 'config');
|
| 37 |
+
const source = path.resolve(configDir, 'github-app-routing.example.json');
|
| 38 |
+
const target = path.resolve(configDir, args.target || 'github-app-routing.local.json');
|
| 39 |
+
const force = args.force === 'true';
|
| 40 |
+
|
| 41 |
+
await mkdir(configDir, { recursive: true });
|
| 42 |
+
if (!force && (await exists(target))) {
|
| 43 |
+
console.log(JSON.stringify({ ok: true, created: false, source, target, message: 'local routing config already exists' }, null, 2));
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
await copyFile(source, target);
|
| 48 |
+
console.log(JSON.stringify({ ok: true, created: true, source, target }, null, 2));
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
main().catch((error) => {
|
| 52 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 53 |
+
process.exit(1);
|
| 54 |
+
});
|
scripts/render-github-app-env-template.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { readFile } from 'node:fs/promises';
|
| 4 |
+
import path from 'node:path';
|
| 5 |
+
|
| 6 |
+
const cwd = process.cwd();
|
| 7 |
+
|
| 8 |
+
function parseArgs(argv) {
|
| 9 |
+
const out = {};
|
| 10 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 11 |
+
const token = argv[i];
|
| 12 |
+
if (!token.startsWith('--')) continue;
|
| 13 |
+
const key = token.slice(2);
|
| 14 |
+
const next = argv[i + 1];
|
| 15 |
+
if (!next || next.startsWith('--')) {
|
| 16 |
+
out[key] = 'true';
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
out[key] = next;
|
| 20 |
+
i += 1;
|
| 21 |
+
}
|
| 22 |
+
return out;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function firstString(...values) {
|
| 26 |
+
for (const value of values) {
|
| 27 |
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
| 28 |
+
}
|
| 29 |
+
return null;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function parseList(value) {
|
| 33 |
+
if (!value || typeof value !== 'string') return [];
|
| 34 |
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function asPlaceholder(name) {
|
| 38 |
+
return `<SET_${name}>`;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
async function main() {
|
| 42 |
+
const args = parseArgs(process.argv.slice(2));
|
| 43 |
+
const format = firstString(args.format, 'dotenv');
|
| 44 |
+
const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
|
| 45 |
+
const selectedAgents = new Set(parseList(args.agents || args.agent));
|
| 46 |
+
const raw = await readFile(configPath, 'utf8');
|
| 47 |
+
const config = JSON.parse(raw);
|
| 48 |
+
const apps = Array.isArray(config.apps) ? config.apps : [];
|
| 49 |
+
|
| 50 |
+
const envEntries = new Map([
|
| 51 |
+
['SIN_GITHUB_APP_ROUTING_PATH', './config/github-app-routing.local.json'],
|
| 52 |
+
['SIN_GITHUB_API_BASE_URL', 'https://api.github.com'],
|
| 53 |
+
]);
|
| 54 |
+
const appSummaries = [];
|
| 55 |
+
|
| 56 |
+
for (const app of apps) {
|
| 57 |
+
const agentSlug = firstString(app.agentSlug, app.preferredAgentSlug, app.appSlug);
|
| 58 |
+
const appSlug = firstString(app.appSlug, agentSlug);
|
| 59 |
+
if (selectedAgents.size > 0 && !selectedAgents.has(agentSlug || '') && !selectedAgents.has(appSlug || '')) continue;
|
| 60 |
+
|
| 61 |
+
const trackedKeys = [
|
| 62 |
+
firstString(app.appIdEnv),
|
| 63 |
+
firstString(app.clientIdEnv),
|
| 64 |
+
firstString(app.privateKeyEnv),
|
| 65 |
+
firstString(app.webhookSecretEnv),
|
| 66 |
+
].filter(Boolean);
|
| 67 |
+
|
| 68 |
+
for (const key of trackedKeys) envEntries.set(key, asPlaceholder(key));
|
| 69 |
+
|
| 70 |
+
appSummaries.push({
|
| 71 |
+
agentSlug,
|
| 72 |
+
appSlug,
|
| 73 |
+
botName: firstString(app.botName),
|
| 74 |
+
routePath: firstString(app.routePath, app.webhookPath, config.defaultWebhookPath, '/github/webhook'),
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (format === 'json') {
|
| 79 |
+
process.stdout.write(JSON.stringify({ configPath, env: Object.fromEntries(envEntries), appSummaries }, null, 2) + '\n');
|
| 80 |
+
return;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const lines = [];
|
| 84 |
+
if (format === 'shell') {
|
| 85 |
+
for (const [key, value] of envEntries.entries()) lines.push(`export ${key}=${JSON.stringify(value)}`);
|
| 86 |
+
} else {
|
| 87 |
+
for (const [key, value] of envEntries.entries()) lines.push(`${key}=${value}`);
|
| 88 |
+
}
|
| 89 |
+
if (appSummaries.length > 0) {
|
| 90 |
+
lines.push('');
|
| 91 |
+
lines.push('# app summaries');
|
| 92 |
+
for (const summary of appSummaries) {
|
| 93 |
+
lines.push(`${summary.agentSlug} -> ${summary.botName || summary.appSlug} -> ${summary.routePath}`);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
process.stdout.write(lines.join('\n') + '\n');
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
main().catch((error) => {
|
| 100 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 101 |
+
process.exit(1);
|
| 102 |
+
});
|
scripts/render-github-app-vm-rollout.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { readFile } from 'node:fs/promises';
|
| 4 |
+
import path from 'node:path';
|
| 5 |
+
|
| 6 |
+
const cwd = process.cwd();
|
| 7 |
+
|
| 8 |
+
function parseArgs(argv) {
|
| 9 |
+
const out = {};
|
| 10 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 11 |
+
const token = argv[i];
|
| 12 |
+
if (!token.startsWith('--')) continue;
|
| 13 |
+
const key = token.slice(2);
|
| 14 |
+
const next = argv[i + 1];
|
| 15 |
+
if (!next || next.startsWith('--')) {
|
| 16 |
+
out[key] = 'true';
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
out[key] = next;
|
| 20 |
+
i += 1;
|
| 21 |
+
}
|
| 22 |
+
return out;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function firstString(...values) {
|
| 26 |
+
for (const value of values) {
|
| 27 |
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
| 28 |
+
}
|
| 29 |
+
return null;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function parseList(value) {
|
| 33 |
+
if (!value || typeof value !== 'string') return [];
|
| 34 |
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async function main() {
|
| 38 |
+
const args = parseArgs(process.argv.slice(2));
|
| 39 |
+
const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
|
| 40 |
+
const baseUrl = firstString(args['base-url'], 'https://<SET_VM_BASE_URL>');
|
| 41 |
+
const repo = firstString(args.repo, 'OpenSIN-AI/OpenSIN-backend');
|
| 42 |
+
const issueNumber = firstString(args['issue-number'], '<SET_TEST_ISSUE_NUMBER>');
|
| 43 |
+
const installationId = firstString(args['installation-id'], '<SET_INSTALLATION_ID>');
|
| 44 |
+
const selected = new Set(parseList(args.agents || args.agent));
|
| 45 |
+
|
| 46 |
+
const raw = await readFile(configPath, 'utf8');
|
| 47 |
+
const config = JSON.parse(raw);
|
| 48 |
+
const apps = Array.isArray(config.apps) ? config.apps : [];
|
| 49 |
+
const rolloutAgents = apps
|
| 50 |
+
.map((entry) => ({
|
| 51 |
+
agentSlug: firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug),
|
| 52 |
+
appSlug: firstString(entry.appSlug, entry.agentSlug, entry.preferredAgentSlug),
|
| 53 |
+
botName: firstString(entry.botName),
|
| 54 |
+
routePath: firstString(entry.routePath, entry.webhookPath, config.defaultWebhookPath, '/github/webhook'),
|
| 55 |
+
}))
|
| 56 |
+
.filter((entry) => entry.agentSlug && entry.appSlug)
|
| 57 |
+
.filter((entry) => selected.size === 0 || selected.has(entry.agentSlug) || selected.has(entry.appSlug));
|
| 58 |
+
|
| 59 |
+
const agentArgs = rolloutAgents.map((entry) => entry.appSlug).join(',');
|
| 60 |
+
const lines = [
|
| 61 |
+
'# GitHub App VM Rollout Checklist',
|
| 62 |
+
'',
|
| 63 |
+
'cd a2a/team-coding/A2A-SIN-GitHub-Issues',
|
| 64 |
+
'npm run build',
|
| 65 |
+
'npm run ops:github-app-routing:init',
|
| 66 |
+
`export SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json`,
|
| 67 |
+
'export SIN_GITHUB_API_BASE_URL=https://api.github.com',
|
| 68 |
+
'',
|
| 69 |
+
'# Fill these VM env vars',
|
| 70 |
+
];
|
| 71 |
+
|
| 72 |
+
for (const entry of rolloutAgents) {
|
| 73 |
+
const prefix = String(entry.appSlug).toUpperCase().replace(/-/g, '_');
|
| 74 |
+
lines.push(`export ${prefix}_GITHUB_APP_ID=<SET_${prefix}_GITHUB_APP_ID>`);
|
| 75 |
+
lines.push(`export ${prefix}_GITHUB_APP_CLIENT_ID=<SET_${prefix}_GITHUB_APP_CLIENT_ID>`);
|
| 76 |
+
lines.push(`export ${prefix}_GITHUB_APP_PRIVATE_KEY_PEM='<SET_${prefix}_GITHUB_APP_PRIVATE_KEY_PEM>'`);
|
| 77 |
+
lines.push(`export ${prefix}_GITHUB_APP_WEBHOOK_SECRET=<SET_${prefix}_GITHUB_APP_WEBHOOK_SECRET>`);
|
| 78 |
+
lines.push('');
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
lines.push(`# Validate only the configured rollout targets`);
|
| 82 |
+
lines.push(`npm run ops:github-app-routing:check -- --configured-only true --agents ${agentArgs}`);
|
| 83 |
+
lines.push('');
|
| 84 |
+
lines.push('# Start the GitHub Issues A2A runtime');
|
| 85 |
+
lines.push('node dist/src/cli.js serve-a2a');
|
| 86 |
+
lines.push('');
|
| 87 |
+
lines.push('# In a second shell, run signed webhook smoke tests');
|
| 88 |
+
for (const entry of rolloutAgents) {
|
| 89 |
+
lines.push(`npm run ops:github-app-routing:smoke -- --agent ${entry.appSlug} --base-url ${baseUrl}`);
|
| 90 |
+
}
|
| 91 |
+
lines.push('');
|
| 92 |
+
lines.push('# Optional: live comment test as each bot identity');
|
| 93 |
+
for (const entry of rolloutAgents) {
|
| 94 |
+
lines.push(`npm run ops:github-app-comment -- --agent ${entry.agentSlug} --repo ${repo} --issue-number ${issueNumber} --installation-id ${installationId} --body "Hello from ${entry.botName}"`);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
process.stdout.write(lines.join('\n') + '\n');
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
main().catch((error) => {
|
| 101 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 102 |
+
process.exit(1);
|
| 103 |
+
});
|
scripts/smoke-github-app-routing.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import { createHmac } from 'node:crypto';
|
| 4 |
+
import { readFile } from 'node:fs/promises';
|
| 5 |
+
import path from 'node:path';
|
| 6 |
+
|
| 7 |
+
const cwd = process.cwd();
|
| 8 |
+
|
| 9 |
+
function parseArgs(argv) {
|
| 10 |
+
const out = {};
|
| 11 |
+
for (let i = 0; i < argv.length; i += 1) {
|
| 12 |
+
const token = argv[i];
|
| 13 |
+
if (!token.startsWith('--')) continue;
|
| 14 |
+
const key = token.slice(2);
|
| 15 |
+
const next = argv[i + 1];
|
| 16 |
+
if (!next || next.startsWith('--')) {
|
| 17 |
+
out[key] = 'true';
|
| 18 |
+
continue;
|
| 19 |
+
}
|
| 20 |
+
out[key] = next;
|
| 21 |
+
i += 1;
|
| 22 |
+
}
|
| 23 |
+
return out;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function firstString(...values) {
|
| 27 |
+
for (const value of values) {
|
| 28 |
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
| 29 |
+
}
|
| 30 |
+
return null;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function readEnv(name) {
|
| 34 |
+
if (!name) return null;
|
| 35 |
+
const value = process.env[name];
|
| 36 |
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function normalizeRoute(entry, defaultWebhookPath) {
|
| 40 |
+
return {
|
| 41 |
+
agentSlug: firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug),
|
| 42 |
+
appSlug: firstString(entry.appSlug),
|
| 43 |
+
botName: firstString(entry.botName),
|
| 44 |
+
appId: Number(firstString(String(entry.appId || '').trim(), readEnv(entry.appIdEnv))),
|
| 45 |
+
webhookPath: firstString(entry.routePath, entry.webhookPath, defaultWebhookPath || '/github/webhook'),
|
| 46 |
+
webhookSecret: firstString(entry.webhookSecret, readEnv(entry.webhookSecretEnv)),
|
| 47 |
+
};
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
async function main() {
|
| 51 |
+
const args = parseArgs(process.argv.slice(2));
|
| 52 |
+
const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
|
| 53 |
+
const baseUrl = firstString(args['base-url'], process.env.SIN_GITHUB_ISSUES_PUBLIC_BASE_URL, 'http://127.0.0.1:46031');
|
| 54 |
+
const agent = firstString(args.agent, args.slug, args['app-slug']);
|
| 55 |
+
if (!agent) throw new Error('missing_required_arg: --agent <slug-or-app-slug>');
|
| 56 |
+
|
| 57 |
+
const raw = await readFile(configPath, 'utf8');
|
| 58 |
+
const config = JSON.parse(raw);
|
| 59 |
+
const defaultWebhookPath = firstString(config.defaultWebhookPath, '/github/webhook');
|
| 60 |
+
const apps = Array.isArray(config.apps) ? config.apps.map((entry) => normalizeRoute(entry, defaultWebhookPath)) : [];
|
| 61 |
+
const target = apps.find((entry) => entry.agentSlug === agent || entry.appSlug === agent);
|
| 62 |
+
if (!target) throw new Error(`unknown_agent_route:${agent}`);
|
| 63 |
+
if (!target.webhookSecret) throw new Error(`missing_webhook_secret:${agent}`);
|
| 64 |
+
if (!Number.isFinite(target.appId)) throw new Error(`missing_app_id:${agent}`);
|
| 65 |
+
|
| 66 |
+
const payload = JSON.stringify({
|
| 67 |
+
action: args.action || 'opened',
|
| 68 |
+
installation: { id: Number(args['installation-id'] || 987654321), app_id: target.appId },
|
| 69 |
+
repository: { full_name: args.repo || 'OpenSIN-AI/OpenSIN-backend' },
|
| 70 |
+
});
|
| 71 |
+
const signature256 = `sha256=${createHmac('sha256', target.webhookSecret).update(payload).digest('hex')}`;
|
| 72 |
+
const response = await fetch(`${baseUrl.replace(/\/+$/, '')}${target.webhookPath}`, {
|
| 73 |
+
method: 'POST',
|
| 74 |
+
headers: {
|
| 75 |
+
'content-type': 'application/json',
|
| 76 |
+
'x-hub-signature-256': signature256,
|
| 77 |
+
'x-github-event': args.event || 'pull_request',
|
| 78 |
+
'x-github-delivery': args.delivery || `smoke-${target.appSlug || target.agentSlug}`,
|
| 79 |
+
},
|
| 80 |
+
body: payload,
|
| 81 |
+
});
|
| 82 |
+
const text = await response.text();
|
| 83 |
+
console.log(JSON.stringify({
|
| 84 |
+
ok: response.ok,
|
| 85 |
+
baseUrl,
|
| 86 |
+
routePath: target.webhookPath,
|
| 87 |
+
agentSlug: target.agentSlug,
|
| 88 |
+
appSlug: target.appSlug,
|
| 89 |
+
appId: target.appId,
|
| 90 |
+
status: response.status,
|
| 91 |
+
response: text ? JSON.parse(text) : null,
|
| 92 |
+
}, null, 2));
|
| 93 |
+
if (!response.ok) process.exitCode = 1;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
main().catch((error) => {
|
| 97 |
+
console.error(error instanceof Error ? error.message : String(error));
|
| 98 |
+
process.exit(1);
|
| 99 |
+
});
|
scripts/smoke-room13-worker.mjs
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env node
|
| 2 |
-
|
| 3 |
-
import { GitHubIssuesRoom13WorkerRuntime, installGitHubIssuesWorkerRuntime } from '../dist/src/room13-worker.js';
|
| 4 |
-
|
| 5 |
-
class FakeWorkerRuntime extends GitHubIssuesRoom13WorkerRuntime {
|
| 6 |
-
constructor() {
|
| 7 |
-
super('sin-github-issues', 'http://room13.test');
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
async start() {
|
| 11 |
-
this.workerId = 'worker-gh';
|
| 12 |
-
this.leaseEpoch = 1;
|
| 13 |
-
this.claimedTaskId = '';
|
| 14 |
-
this.leaseExpiresAt = '2026-03-23T22:30:00Z';
|
| 15 |
-
return this.snapshot();
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
async claimNextTask() {
|
| 19 |
-
this.claimedTaskId = 'task-gh-1';
|
| 20 |
-
return { id: 'task-gh-1', status: 'running' };
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
async checkpoint(step, checkpoint = {}) {
|
| 24 |
-
return { ok: true, step, checkpoint };
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
async completeTask(result = {}) {
|
| 28 |
-
this.claimedTaskId = '';
|
| 29 |
-
return { ok: true, result };
|
| 30 |
-
}
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
const runtime = new FakeWorkerRuntime();
|
| 34 |
-
installGitHubIssuesWorkerRuntime(runtime);
|
| 35 |
-
const registered = await runtime.start();
|
| 36 |
-
const claimed = await runtime.claimNextTask();
|
| 37 |
-
const checkpoint = await runtime.checkpoint('github.issue.sync', { attempt: 1 });
|
| 38 |
-
const completed = await runtime.completeTask({ success: true });
|
| 39 |
-
|
| 40 |
-
if (registered.workerId !== 'worker-gh') throw new Error('worker register smoke failed');
|
| 41 |
-
if (claimed.id !== 'task-gh-1') throw new Error('task claim smoke failed');
|
| 42 |
-
if (checkpoint.step !== 'github.issue.sync') throw new Error('checkpoint smoke failed');
|
| 43 |
-
if (!completed.ok) throw new Error('complete smoke failed');
|
| 44 |
-
|
| 45 |
-
process.stdout.write(JSON.stringify({ ok: true, registered, claimed, checkpoint, completed }, null, 2) + '\n');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/smoke-triage-fixtures.mjs
DELETED
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env node
|
| 2 |
-
|
| 3 |
-
import { readFile } from 'node:fs/promises';
|
| 4 |
-
import path from 'node:path';
|
| 5 |
-
import process from 'node:process';
|
| 6 |
-
|
| 7 |
-
const packageRoot = path.resolve(process.cwd());
|
| 8 |
-
const repoRoot = path.resolve(packageRoot, '..', '..', '..');
|
| 9 |
-
|
| 10 |
-
const fixtureOnePath = path.join(repoRoot, 'Docs', 'operations', 'phase22-github-issue-opened.fixture.json');
|
| 11 |
-
const fixtureTwoPath = path.join(repoRoot, 'Docs', 'operations', 'phase22-github-issue-opened.replay.fixture.json');
|
| 12 |
-
|
| 13 |
-
const triageModule = await import(path.join(packageRoot, 'dist', 'src', 'triage.js'));
|
| 14 |
-
const queueModule = await import(path.join(packageRoot, 'dist', 'src', 'queue-state.js'));
|
| 15 |
-
|
| 16 |
-
const {
|
| 17 |
-
TriageEnqueueSchema,
|
| 18 |
-
buildDeliveryKey,
|
| 19 |
-
buildIssueKey,
|
| 20 |
-
routeIssueTriage,
|
| 21 |
-
} = triageModule;
|
| 22 |
-
|
| 23 |
-
const { ISSUE_QUEUE_STATE } = queueModule;
|
| 24 |
-
|
| 25 |
-
const rowByDelivery = new Map();
|
| 26 |
-
const rowByIssueKey = new Map();
|
| 27 |
-
|
| 28 |
-
function enqueue(payload) {
|
| 29 |
-
const input = TriageEnqueueSchema.parse(payload);
|
| 30 |
-
const deliveryId = buildDeliveryKey(input);
|
| 31 |
-
const issueKey = buildIssueKey(input.repo, input.issueNumber);
|
| 32 |
-
const route = routeIssueTriage(input.labels || [], input.title, input.body || '');
|
| 33 |
-
|
| 34 |
-
if (rowByDelivery.has(deliveryId)) {
|
| 35 |
-
return {
|
| 36 |
-
created: false,
|
| 37 |
-
deduplicatedBy: 'delivery_id',
|
| 38 |
-
queueState: rowByDelivery.get(deliveryId).queueState,
|
| 39 |
-
deliveryId,
|
| 40 |
-
issueKey,
|
| 41 |
-
route,
|
| 42 |
-
};
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
if (rowByIssueKey.has(issueKey)) {
|
| 46 |
-
const existing = rowByIssueKey.get(issueKey);
|
| 47 |
-
existing.deliveryIds.push(deliveryId);
|
| 48 |
-
rowByDelivery.set(deliveryId, existing);
|
| 49 |
-
return {
|
| 50 |
-
created: false,
|
| 51 |
-
deduplicatedBy: 'issue_key',
|
| 52 |
-
queueState: existing.queueState,
|
| 53 |
-
deliveryId,
|
| 54 |
-
issueKey,
|
| 55 |
-
route,
|
| 56 |
-
};
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
const row = {
|
| 60 |
-
issueKey,
|
| 61 |
-
deliveryIds: [deliveryId],
|
| 62 |
-
queueState: ISSUE_QUEUE_STATE.queued,
|
| 63 |
-
assignedTeam: input.assignedTeam || route.assignedTeam,
|
| 64 |
-
route,
|
| 65 |
-
};
|
| 66 |
-
rowByDelivery.set(deliveryId, row);
|
| 67 |
-
rowByIssueKey.set(issueKey, row);
|
| 68 |
-
return {
|
| 69 |
-
created: true,
|
| 70 |
-
deduplicatedBy: null,
|
| 71 |
-
queueState: row.queueState,
|
| 72 |
-
deliveryId,
|
| 73 |
-
issueKey,
|
| 74 |
-
route,
|
| 75 |
-
};
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
const first = enqueue(JSON.parse(await readFile(fixtureOnePath, 'utf8')));
|
| 79 |
-
const replay = enqueue(JSON.parse(await readFile(fixtureTwoPath, 'utf8')));
|
| 80 |
-
|
| 81 |
-
if (!first.created) throw new Error('expected first fixture to create queue row');
|
| 82 |
-
if (replay.created || replay.deduplicatedBy !== 'delivery_id') throw new Error('expected replay fixture to deduplicate by delivery_id');
|
| 83 |
-
if (rowByIssueKey.size !== 1) throw new Error(`expected exactly one queue row, got ${rowByIssueKey.size}`);
|
| 84 |
-
|
| 85 |
-
process.stdout.write(JSON.stringify({ ok: true, createdRows: rowByIssueKey.size, first, replay, finalRow: [...rowByIssueKey.values()][0] }, null, 2) + '\n');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/a2a-http.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { randomUUID } from 'node:crypto';
|
| 2 |
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
| 3 |
import type { GitHubAgentAction } from './runtime.js';
|
|
|
|
| 4 |
import { buildAgentCard, resolveTemplateAgentConfig, TEMPLATE_AGENT_ID, TEMPLATE_AGENT_NAME } from './metadata.js';
|
| 5 |
import { executeGitHubAgentAction } from './runtime.js';
|
| 6 |
|
|
@@ -24,18 +25,28 @@ export function createTemplateAgentHttpServer() {
|
|
| 24 |
}
|
| 25 |
|
| 26 |
async function handleRequest(request: IncomingMessage, response: ServerResponse, baseUrl: string) {
|
|
|
|
| 27 |
if (request.method === 'GET' && request.url === '/health') return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID });
|
| 28 |
if (request.method === 'GET' && request.url === '/') return sendHtml(response, 200, `<html><body><h1>${TEMPLATE_AGENT_NAME}</h1></body></html>`);
|
| 29 |
if (request.method === 'GET' && (request.url === '/.well-known/agent-card.json' || request.url === '/.well-known/agent.json')) {
|
| 30 |
return sendJson(response, 200, buildAgentCard(baseUrl));
|
| 31 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
if (request.method === 'POST' && request.url === '/a2a/v1') {
|
| 33 |
const rpc = ((await readJson(request)) || {}) as RpcRequest;
|
| 34 |
-
if (rpc && typeof rpc === 'object' && 'action' in rpc && typeof (rpc as { action?: unknown }).action === 'string') {
|
| 35 |
-
const action = rpc as unknown as GitHubAgentAction;
|
| 36 |
-
const result = await executeGitHubAgentAction(action);
|
| 37 |
-
return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID, action: action.action, result });
|
| 38 |
-
}
|
| 39 |
if (rpc.method === 'agent/getCard') return sendJson(response, 200, { jsonrpc: '2.0', id: rpc.id ?? null, result: buildAgentCard(baseUrl) });
|
| 40 |
if (rpc.method === 'message/send') {
|
| 41 |
const action = parseAction((rpc.params?.message as { parts?: Array<{ text?: string }> } | undefined)?.parts?.map((part) => part.text || '').join(' ').trim() || '');
|
|
@@ -76,10 +87,23 @@ function parseAction(text: string): GitHubAgentAction {
|
|
| 76 |
}
|
| 77 |
|
| 78 |
async function readJson(request: IncomingMessage) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
const chunks: Buffer[] = [];
|
| 80 |
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
function sendJson(response: ServerResponse, statusCode: number, payload: unknown) {
|
|
|
|
| 1 |
import { randomUUID } from 'node:crypto';
|
| 2 |
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
| 3 |
import type { GitHubAgentAction } from './runtime.js';
|
| 4 |
+
import { listGitHubAppWebhookPaths, routeGitHubWebhook } from './github-app-routing.js';
|
| 5 |
import { buildAgentCard, resolveTemplateAgentConfig, TEMPLATE_AGENT_ID, TEMPLATE_AGENT_NAME } from './metadata.js';
|
| 6 |
import { executeGitHubAgentAction } from './runtime.js';
|
| 7 |
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
async function handleRequest(request: IncomingMessage, response: ServerResponse, baseUrl: string) {
|
| 28 |
+
const routePath = normalizePath(request.url || '/');
|
| 29 |
if (request.method === 'GET' && request.url === '/health') return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID });
|
| 30 |
if (request.method === 'GET' && request.url === '/') return sendHtml(response, 200, `<html><body><h1>${TEMPLATE_AGENT_NAME}</h1></body></html>`);
|
| 31 |
if (request.method === 'GET' && (request.url === '/.well-known/agent-card.json' || request.url === '/.well-known/agent.json')) {
|
| 32 |
return sendJson(response, 200, buildAgentCard(baseUrl));
|
| 33 |
}
|
| 34 |
+
const configuredWebhookPaths = request.method === 'POST' ? await listGitHubAppWebhookPaths() : [];
|
| 35 |
+
if (request.method === 'POST' && configuredWebhookPaths.some((configuredPath) => routePath === configuredPath || routePath.startsWith(`${configuredPath}/`))) {
|
| 36 |
+
const rawBody = await readRawBody(request);
|
| 37 |
+
const payload = rawBody.trim() ? JSON.parse(rawBody) : {};
|
| 38 |
+
const result = await routeGitHubWebhook({
|
| 39 |
+
payload,
|
| 40 |
+
rawBody,
|
| 41 |
+
routePath,
|
| 42 |
+
signature256: readHeader(request, 'x-hub-signature-256'),
|
| 43 |
+
eventName: readHeader(request, 'x-github-event'),
|
| 44 |
+
deliveryId: readHeader(request, 'x-github-delivery'),
|
| 45 |
+
});
|
| 46 |
+
return sendJson(response, result.ok ? 200 : 400, result);
|
| 47 |
+
}
|
| 48 |
if (request.method === 'POST' && request.url === '/a2a/v1') {
|
| 49 |
const rpc = ((await readJson(request)) || {}) as RpcRequest;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
if (rpc.method === 'agent/getCard') return sendJson(response, 200, { jsonrpc: '2.0', id: rpc.id ?? null, result: buildAgentCard(baseUrl) });
|
| 51 |
if (rpc.method === 'message/send') {
|
| 52 |
const action = parseAction((rpc.params?.message as { parts?: Array<{ text?: string }> } | undefined)?.parts?.map((part) => part.text || '').join(' ').trim() || '');
|
|
|
|
| 87 |
}
|
| 88 |
|
| 89 |
async function readJson(request: IncomingMessage) {
|
| 90 |
+
const raw = (await readRawBody(request)).trim();
|
| 91 |
+
return raw ? JSON.parse(raw) : null;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
async function readRawBody(request: IncomingMessage) {
|
| 95 |
const chunks: Buffer[] = [];
|
| 96 |
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
| 97 |
+
return Buffer.concat(chunks).toString('utf8');
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function readHeader(request: IncomingMessage, name: string) {
|
| 101 |
+
const value = request.headers[name];
|
| 102 |
+
return typeof value === 'string' ? value : Array.isArray(value) ? value.join(',') : undefined;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function normalizePath(url: string) {
|
| 106 |
+
return url.split('?', 1)[0] || '/';
|
| 107 |
}
|
| 108 |
|
| 109 |
function sendJson(response: ServerResponse, statusCode: number, payload: unknown) {
|
src/cli.ts
CHANGED
|
@@ -5,22 +5,11 @@ import { resolve } from 'node:path';
|
|
| 5 |
import { createTemplateAgentHttpServer } from './a2a-http.js';
|
| 6 |
import { runMcpServer } from './mcp-server.js';
|
| 7 |
import { executeGitHubAgentAction } from './runtime.js';
|
| 8 |
-
import { GitHubIssuesRoom13WorkerRuntime, installGitHubIssuesWorkerRuntime } from './room13-worker.js';
|
| 9 |
|
| 10 |
const cmd = process.argv[2];
|
| 11 |
|
| 12 |
if (cmd === 'serve-a2a') {
|
| 13 |
-
createTemplateAgentHttpServer().start().
|
| 14 |
-
const room13Worker = new GitHubIssuesRoom13WorkerRuntime('sin-github-issues');
|
| 15 |
-
installGitHubIssuesWorkerRuntime(room13Worker);
|
| 16 |
-
await room13Worker.start().catch(() => undefined);
|
| 17 |
-
const stop = async () => {
|
| 18 |
-
await room13Worker.stop().catch(() => undefined);
|
| 19 |
-
process.exit(0);
|
| 20 |
-
};
|
| 21 |
-
process.once('SIGINT', () => void stop());
|
| 22 |
-
process.once('SIGTERM', () => void stop());
|
| 23 |
-
}).catch(err => {
|
| 24 |
console.error(err);
|
| 25 |
process.exit(1);
|
| 26 |
});
|
|
|
|
| 5 |
import { createTemplateAgentHttpServer } from './a2a-http.js';
|
| 6 |
import { runMcpServer } from './mcp-server.js';
|
| 7 |
import { executeGitHubAgentAction } from './runtime.js';
|
|
|
|
| 8 |
|
| 9 |
const cmd = process.argv[2];
|
| 10 |
|
| 11 |
if (cmd === 'serve-a2a') {
|
| 12 |
+
createTemplateAgentHttpServer().start().catch(err => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
console.error(err);
|
| 14 |
process.exit(1);
|
| 15 |
});
|
src/github-app-routing.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createHmac, createPrivateKey, createSign, timingSafeEqual } from 'node:crypto';
|
| 2 |
+
import { readFile } from 'node:fs/promises';
|
| 3 |
+
|
| 4 |
+
type JsonObject = Record<string, unknown>;
|
| 5 |
+
|
| 6 |
+
export type GitHubAppRouteRecord = {
|
| 7 |
+
agentSlug: string;
|
| 8 |
+
botName?: string;
|
| 9 |
+
appId: number;
|
| 10 |
+
clientId?: string;
|
| 11 |
+
appSlug?: string;
|
| 12 |
+
routePath?: string;
|
| 13 |
+
privateKeyPem?: string;
|
| 14 |
+
privateKeyPath?: string;
|
| 15 |
+
privateKeyEnv?: string;
|
| 16 |
+
webhookSecret?: string;
|
| 17 |
+
webhookSecretPath?: string;
|
| 18 |
+
webhookSecretEnv?: string;
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
type GitHubAppRoutingConfig = {
|
| 22 |
+
defaultMode?: 'central' | 'path' | 'hybrid';
|
| 23 |
+
defaultWebhookPath?: string;
|
| 24 |
+
apps: GitHubAppRouteRecord[];
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
type WebhookRouteInput = {
|
| 28 |
+
payload?: unknown;
|
| 29 |
+
rawBody?: string;
|
| 30 |
+
routePath?: string;
|
| 31 |
+
signature256?: string;
|
| 32 |
+
eventName?: string;
|
| 33 |
+
deliveryId?: string;
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
type CommentAsAppInput = {
|
| 37 |
+
repo: string;
|
| 38 |
+
issueNumber: number;
|
| 39 |
+
body: string;
|
| 40 |
+
agentSlug?: string;
|
| 41 |
+
appId?: number;
|
| 42 |
+
installationId: number;
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const GITHUB_API_BASE = String(process.env.SIN_GITHUB_API_BASE_URL || 'https://api.github.com').trim() || 'https://api.github.com';
|
| 46 |
+
const GITHUB_API_VERSION = '2022-11-28';
|
| 47 |
+
|
| 48 |
+
export async function getGitHubAppRoutingStatus() {
|
| 49 |
+
const config = await loadGitHubAppRoutingConfig();
|
| 50 |
+
const configuredApps = await Promise.all(
|
| 51 |
+
config.apps.map(async (app) => {
|
| 52 |
+
const privateKeyResolved = Boolean(await readPrivateKey(app));
|
| 53 |
+
const webhookSecretResolved = Boolean(await readWebhookSecret(app));
|
| 54 |
+
return {
|
| 55 |
+
agentSlug: app.agentSlug,
|
| 56 |
+
appSlug: app.appSlug || null,
|
| 57 |
+
botName: app.botName || `${app.agentSlug}[bot]`,
|
| 58 |
+
appId: app.appId,
|
| 59 |
+
clientId: app.clientId || null,
|
| 60 |
+
routePath: app.routePath || `${config.defaultWebhookPath || '/github/webhook'}/${app.agentSlug}`,
|
| 61 |
+
privateKeyRefConfigured: Boolean(app.privateKeyPem || app.privateKeyPath || app.privateKeyEnv),
|
| 62 |
+
privateKeyResolved,
|
| 63 |
+
webhookSecretRefConfigured: Boolean(app.webhookSecret || app.webhookSecretPath || app.webhookSecretEnv),
|
| 64 |
+
webhookSecretResolved,
|
| 65 |
+
};
|
| 66 |
+
}),
|
| 67 |
+
);
|
| 68 |
+
return {
|
| 69 |
+
ok: true,
|
| 70 |
+
defaultMode: config.defaultMode || 'hybrid',
|
| 71 |
+
defaultWebhookPath: config.defaultWebhookPath || '/github/webhook',
|
| 72 |
+
configuredApps,
|
| 73 |
+
};
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export async function listGitHubAppWebhookPaths() {
|
| 77 |
+
const config = await loadGitHubAppRoutingConfig();
|
| 78 |
+
const paths = new Set<string>();
|
| 79 |
+
paths.add(config.defaultWebhookPath || '/github/webhook');
|
| 80 |
+
for (const app of config.apps) {
|
| 81 |
+
if (app.routePath) paths.add(normalizeRoutePath(app.routePath));
|
| 82 |
+
}
|
| 83 |
+
return [...paths];
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export async function routeGitHubWebhook(input: WebhookRouteInput) {
|
| 87 |
+
const config = await loadGitHubAppRoutingConfig();
|
| 88 |
+
const payload = parsePayload(input.payload, input.rawBody);
|
| 89 |
+
const installation = asObject(payload.installation);
|
| 90 |
+
const installationId = parseInteger(installation?.id);
|
| 91 |
+
const appId = parseInteger(installation?.app_id);
|
| 92 |
+
const routePath = normalizeRoutePath(input.routePath || config.defaultWebhookPath || '/github/webhook');
|
| 93 |
+
const matchedApp = resolveRoute(config, { appId, routePath });
|
| 94 |
+
|
| 95 |
+
if (!matchedApp) {
|
| 96 |
+
return {
|
| 97 |
+
ok: false,
|
| 98 |
+
error: 'github_app_route_not_found',
|
| 99 |
+
routePath,
|
| 100 |
+
appId,
|
| 101 |
+
installationId,
|
| 102 |
+
configuredAppIds: config.apps.map((app) => app.appId),
|
| 103 |
+
};
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
const verification = await verifyWebhookSignature(matchedApp, input.signature256, input.rawBody || '');
|
| 107 |
+
if (!verification.ok) {
|
| 108 |
+
return {
|
| 109 |
+
ok: false,
|
| 110 |
+
error: verification.error,
|
| 111 |
+
routePath,
|
| 112 |
+
appId,
|
| 113 |
+
installationId,
|
| 114 |
+
agentSlug: matchedApp.agentSlug,
|
| 115 |
+
botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
const repository = asObject(payload.repository);
|
| 120 |
+
return {
|
| 121 |
+
ok: true,
|
| 122 |
+
route: {
|
| 123 |
+
agentSlug: matchedApp.agentSlug,
|
| 124 |
+
botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
|
| 125 |
+
appId: matchedApp.appId,
|
| 126 |
+
installationId,
|
| 127 |
+
routePath: matchedApp.routePath || `${config.defaultWebhookPath || '/github/webhook'}/${matchedApp.agentSlug}`,
|
| 128 |
+
},
|
| 129 |
+
event: {
|
| 130 |
+
name: input.eventName || null,
|
| 131 |
+
deliveryId: input.deliveryId || null,
|
| 132 |
+
action: typeof payload.action === 'string' ? payload.action : null,
|
| 133 |
+
repository: typeof repository?.full_name === 'string' ? repository.full_name : null,
|
| 134 |
+
},
|
| 135 |
+
verification: {
|
| 136 |
+
signatureChecked: Boolean(input.signature256),
|
| 137 |
+
signatureValid: verification.signatureValid,
|
| 138 |
+
},
|
| 139 |
+
};
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
export async function commentIssueAsGitHubApp(input: CommentAsAppInput) {
|
| 143 |
+
const config = await loadGitHubAppRoutingConfig();
|
| 144 |
+
const matchedApp = resolveCommentRoute(config, input);
|
| 145 |
+
if (!matchedApp) {
|
| 146 |
+
throw new Error(`github_app_route_not_found:${input.agentSlug || input.appId || 'unknown'}`);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
const privateKey = await readPrivateKey(matchedApp);
|
| 150 |
+
if (!privateKey) {
|
| 151 |
+
throw new Error(`github_app_private_key_missing:${matchedApp.agentSlug}`);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const jwt = createGitHubAppJwt(matchedApp.appId, privateKey);
|
| 155 |
+
const tokenPayload = await githubApiJson<{ token?: string; expires_at?: string }>(
|
| 156 |
+
`${GITHUB_API_BASE}/app/installations/${input.installationId}/access_tokens`,
|
| 157 |
+
{
|
| 158 |
+
method: 'POST',
|
| 159 |
+
headers: {
|
| 160 |
+
Authorization: `Bearer ${jwt}`,
|
| 161 |
+
Accept: 'application/vnd.github+json',
|
| 162 |
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
| 163 |
+
},
|
| 164 |
+
body: '{}',
|
| 165 |
+
},
|
| 166 |
+
);
|
| 167 |
+
|
| 168 |
+
if (!tokenPayload.token) {
|
| 169 |
+
throw new Error(`github_app_installation_token_missing:${matchedApp.agentSlug}`);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
const commentPayload = await githubApiJson<{ html_url?: string; id?: number }>(
|
| 173 |
+
`${GITHUB_API_BASE}/repos/${input.repo}/issues/${input.issueNumber}/comments`,
|
| 174 |
+
{
|
| 175 |
+
method: 'POST',
|
| 176 |
+
headers: {
|
| 177 |
+
Authorization: `Bearer ${tokenPayload.token}`,
|
| 178 |
+
Accept: 'application/vnd.github+json',
|
| 179 |
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
| 180 |
+
'Content-Type': 'application/json',
|
| 181 |
+
},
|
| 182 |
+
body: JSON.stringify({ body: input.body }),
|
| 183 |
+
},
|
| 184 |
+
);
|
| 185 |
+
|
| 186 |
+
return {
|
| 187 |
+
ok: true,
|
| 188 |
+
repo: input.repo,
|
| 189 |
+
issueNumber: input.issueNumber,
|
| 190 |
+
appId: matchedApp.appId,
|
| 191 |
+
agentSlug: matchedApp.agentSlug,
|
| 192 |
+
botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
|
| 193 |
+
installationId: input.installationId,
|
| 194 |
+
commentId: commentPayload.id || null,
|
| 195 |
+
commentUrl: commentPayload.html_url || null,
|
| 196 |
+
tokenExpiresAt: tokenPayload.expires_at || null,
|
| 197 |
+
};
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function loadGitHubAppRoutingConfig(): Promise<GitHubAppRoutingConfig> {
|
| 201 |
+
const inlineJson = String(process.env.SIN_GITHUB_APP_ROUTING_JSON || '').trim();
|
| 202 |
+
const configPath = String(process.env.SIN_GITHUB_APP_ROUTING_PATH || '').trim();
|
| 203 |
+
|
| 204 |
+
let parsed: unknown = { apps: [] };
|
| 205 |
+
if (inlineJson) {
|
| 206 |
+
parsed = JSON.parse(inlineJson);
|
| 207 |
+
} else if (configPath) {
|
| 208 |
+
parsed = JSON.parse(await readFile(configPath, 'utf8'));
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
const asConfig = asObject(parsed) || { apps: [] };
|
| 212 |
+
const apps = Array.isArray(asConfig.apps)
|
| 213 |
+
? asConfig.apps
|
| 214 |
+
.map((entry) => normalizeAppRecord(entry))
|
| 215 |
+
.filter((entry): entry is GitHubAppRouteRecord => Boolean(entry))
|
| 216 |
+
: [];
|
| 217 |
+
|
| 218 |
+
return {
|
| 219 |
+
defaultMode: parseMode(asConfig.defaultMode ?? asConfig.mode),
|
| 220 |
+
defaultWebhookPath: typeof asConfig.defaultWebhookPath === 'string' ? normalizeRoutePath(asConfig.defaultWebhookPath) : '/github/webhook',
|
| 221 |
+
apps,
|
| 222 |
+
};
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
function normalizeAppRecord(value: unknown): GitHubAppRouteRecord | null {
|
| 226 |
+
const entry = asObject(value);
|
| 227 |
+
if (!entry) return null;
|
| 228 |
+
const agentSlug = firstNonEmptyString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug);
|
| 229 |
+
const appId = parseInteger(entry.appId) ?? parseInteger(resolveEnvString(entry.appIdEnv));
|
| 230 |
+
if (!agentSlug || !appId) return null;
|
| 231 |
+
const routePath = firstNonEmptyString(entry.routePath, entry.webhookPath);
|
| 232 |
+
return {
|
| 233 |
+
agentSlug,
|
| 234 |
+
appSlug: firstNonEmptyString(entry.appSlug) || undefined,
|
| 235 |
+
botName: typeof entry.botName === 'string' ? entry.botName.trim() : undefined,
|
| 236 |
+
appId,
|
| 237 |
+
clientId: firstNonEmptyString(entry.clientId, resolveEnvString(entry.clientIdEnv)) || undefined,
|
| 238 |
+
routePath: routePath ? normalizeRoutePath(routePath) : undefined,
|
| 239 |
+
privateKeyPem: typeof entry.privateKeyPem === 'string' ? entry.privateKeyPem : undefined,
|
| 240 |
+
privateKeyPath: typeof entry.privateKeyPath === 'string' ? entry.privateKeyPath.trim() : undefined,
|
| 241 |
+
privateKeyEnv: firstNonEmptyString(entry.privateKeyEnv) || undefined,
|
| 242 |
+
webhookSecret: typeof entry.webhookSecret === 'string' ? entry.webhookSecret : undefined,
|
| 243 |
+
webhookSecretPath: typeof entry.webhookSecretPath === 'string' ? entry.webhookSecretPath.trim() : undefined,
|
| 244 |
+
webhookSecretEnv: firstNonEmptyString(entry.webhookSecretEnv) || undefined,
|
| 245 |
+
};
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function parsePayload(payload: unknown, rawBody?: string) {
|
| 249 |
+
if (payload && typeof payload === 'object') return payload as JsonObject;
|
| 250 |
+
if (rawBody?.trim()) return JSON.parse(rawBody) as JsonObject;
|
| 251 |
+
return {} as JsonObject;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function resolveRoute(config: GitHubAppRoutingConfig, input: { appId: number | null; routePath: string }) {
|
| 255 |
+
if (input.appId) {
|
| 256 |
+
const byAppId = config.apps.find((app) => app.appId === input.appId);
|
| 257 |
+
if (byAppId) return byAppId;
|
| 258 |
+
}
|
| 259 |
+
const matchesByPath = config.apps.filter((app) => app.routePath && normalizeRoutePath(app.routePath) === input.routePath);
|
| 260 |
+
if (matchesByPath.length === 1) return matchesByPath[0] || null;
|
| 261 |
+
return null;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
function resolveCommentRoute(config: GitHubAppRoutingConfig, input: CommentAsAppInput) {
|
| 265 |
+
if (input.appId) {
|
| 266 |
+
return config.apps.find((app) => app.appId === input.appId) || null;
|
| 267 |
+
}
|
| 268 |
+
if (input.agentSlug) {
|
| 269 |
+
return config.apps.find((app) => app.agentSlug === input.agentSlug) || null;
|
| 270 |
+
}
|
| 271 |
+
return null;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
async function verifyWebhookSignature(app: GitHubAppRouteRecord, signature256: string | undefined, rawBody: string) {
|
| 275 |
+
if (!signature256) {
|
| 276 |
+
return { ok: true, signatureValid: false };
|
| 277 |
+
}
|
| 278 |
+
const secret = await readWebhookSecret(app);
|
| 279 |
+
if (!secret) {
|
| 280 |
+
return { ok: false, signatureValid: false, error: `github_app_webhook_secret_missing:${app.agentSlug}` };
|
| 281 |
+
}
|
| 282 |
+
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
|
| 283 |
+
const valid = safeEqual(expected, signature256.trim());
|
| 284 |
+
return valid
|
| 285 |
+
? { ok: true, signatureValid: true }
|
| 286 |
+
: { ok: false, signatureValid: false, error: 'github_app_webhook_signature_invalid' };
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
async function readPrivateKey(app: GitHubAppRouteRecord) {
|
| 290 |
+
if (app.privateKeyPem?.trim()) return app.privateKeyPem.trim();
|
| 291 |
+
if (app.privateKeyEnv && process.env[app.privateKeyEnv]?.trim()) return String(process.env[app.privateKeyEnv]).trim();
|
| 292 |
+
if (app.privateKeyPath?.trim()) return (await readFile(app.privateKeyPath, 'utf8')).trim();
|
| 293 |
+
return null;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
async function readWebhookSecret(app: GitHubAppRouteRecord) {
|
| 297 |
+
if (app.webhookSecret?.trim()) return app.webhookSecret.trim();
|
| 298 |
+
if (app.webhookSecretEnv && process.env[app.webhookSecretEnv]?.trim()) return String(process.env[app.webhookSecretEnv]).trim();
|
| 299 |
+
if (app.webhookSecretPath?.trim()) return (await readFile(app.webhookSecretPath, 'utf8')).trim();
|
| 300 |
+
return null;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
function createGitHubAppJwt(appId: number, privateKeyPem: string) {
|
| 304 |
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
| 305 |
+
const now = Math.floor(Date.now() / 1000);
|
| 306 |
+
const payload = Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId })).toString('base64url');
|
| 307 |
+
const signingInput = `${header}.${payload}`;
|
| 308 |
+
const signer = createSign('RSA-SHA256');
|
| 309 |
+
signer.update(signingInput);
|
| 310 |
+
signer.end();
|
| 311 |
+
const signature = signer.sign(createPrivateKey(privateKeyPem), 'base64url');
|
| 312 |
+
return `${signingInput}.${signature}`;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
async function githubApiJson<T>(url: string, init: RequestInit): Promise<T> {
|
| 316 |
+
const response = await fetch(url, init);
|
| 317 |
+
const text = await response.text();
|
| 318 |
+
if (!response.ok) {
|
| 319 |
+
throw new Error(`github_api_request_failed:${response.status}:${text.slice(0, 500)}`);
|
| 320 |
+
}
|
| 321 |
+
return text ? (JSON.parse(text) as T) : ({} as T);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function parseMode(value: unknown): 'central' | 'path' | 'hybrid' | undefined {
|
| 325 |
+
if (value === 'central' || value === 'path' || value === 'hybrid') return value;
|
| 326 |
+
if (value === 'central-endpoint-routing') return 'central';
|
| 327 |
+
if (value === 'path-routing') return 'path';
|
| 328 |
+
if (value === 'hybrid-routing') return 'hybrid';
|
| 329 |
+
return undefined;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function normalizeRoutePath(value: string) {
|
| 333 |
+
const clean = value.split('?', 1)[0].trim();
|
| 334 |
+
return clean.startsWith('/') ? clean : `/${clean}`;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
function parseInteger(value: unknown) {
|
| 338 |
+
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
| 339 |
+
return Number.isFinite(parsed) ? parsed : null;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function asObject(value: unknown): JsonObject | null {
|
| 343 |
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : null;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
function safeEqual(left: string, right: string) {
|
| 347 |
+
const leftBuffer = Buffer.from(left);
|
| 348 |
+
const rightBuffer = Buffer.from(right);
|
| 349 |
+
if (leftBuffer.length !== rightBuffer.length) return false;
|
| 350 |
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
function firstNonEmptyString(...values: unknown[]) {
|
| 354 |
+
for (const value of values) {
|
| 355 |
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
| 356 |
+
}
|
| 357 |
+
return null;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
function resolveEnvString(envKey: unknown) {
|
| 361 |
+
if (typeof envKey !== 'string' || !envKey.trim()) return null;
|
| 362 |
+
const value = process.env[envKey.trim()];
|
| 363 |
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
| 364 |
+
}
|
src/hf_sync.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
| 1 |
-
import * as fs from 'node:fs';
|
| 2 |
-
import * as path from 'node:path';
|
| 3 |
-
import * as tar from 'tar';
|
| 4 |
-
import { uploadFile, downloadFile } from '@huggingface/hub';
|
| 5 |
-
import cron from 'node-cron';
|
| 6 |
-
|
| 7 |
-
const AUTH_DIR = path.join(process.cwd(), '.wwebjs_auth');
|
| 8 |
-
const BACKUP_FILE = path.join(process.cwd(), 'auth_backup.tar.gz');
|
| 9 |
-
|
| 10 |
-
const hfToken = process.env.HF_TOKEN;
|
| 11 |
-
const datasetRepo = process.env.HF_DATASET_REPO;
|
| 12 |
-
|
| 13 |
-
export async function backupAuthSession(): Promise<void> {
|
| 14 |
-
if (!hfToken || !datasetRepo) {
|
| 15 |
-
return;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
if (!fs.existsSync(AUTH_DIR)) {
|
| 19 |
-
return;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
try {
|
| 23 |
-
await tar.c({ gzip: true, file: BACKUP_FILE, cwd: process.cwd() }, ['.wwebjs_auth']);
|
| 24 |
-
|
| 25 |
-
const fileBuffer = fs.readFileSync(BACKUP_FILE);
|
| 26 |
-
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
| 27 |
-
|
| 28 |
-
await uploadFile({
|
| 29 |
-
repo: { type: 'dataset', name: datasetRepo },
|
| 30 |
-
credentials: { accessToken: hfToken },
|
| 31 |
-
file: {
|
| 32 |
-
path: 'auth_backup.tar.gz',
|
| 33 |
-
content: blob
|
| 34 |
-
},
|
| 35 |
-
commitTitle: 'Auto-Backup: Session - ' + new Date().toISOString()
|
| 36 |
-
});
|
| 37 |
-
} catch (error) {
|
| 38 |
-
console.error('[HF-Sync] Error during backup:', error);
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
export async function restoreAuthSession(): Promise<void> {
|
| 43 |
-
if (!hfToken || !datasetRepo) {
|
| 44 |
-
return;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
try {
|
| 48 |
-
const response = await downloadFile({
|
| 49 |
-
repo: { type: 'dataset', name: datasetRepo },
|
| 50 |
-
credentials: { accessToken: hfToken },
|
| 51 |
-
path: 'auth_backup.tar.gz'
|
| 52 |
-
});
|
| 53 |
-
|
| 54 |
-
if (!response) {
|
| 55 |
-
return;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
const buffer = await response.arrayBuffer();
|
| 59 |
-
fs.writeFileSync(BACKUP_FILE, Buffer.from(buffer));
|
| 60 |
-
|
| 61 |
-
await tar.x({ file: BACKUP_FILE, cwd: process.cwd() });
|
| 62 |
-
console.log('[HF-Sync] Restored session from dataset.');
|
| 63 |
-
} catch (error: any) {
|
| 64 |
-
if (!error.message?.includes('404')) {
|
| 65 |
-
console.error('[HF-Sync] Restore error:', error);
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export function startKeepAlivePing(): void {
|
| 71 |
-
const spaceUrl = process.env.SPACE_HOST ? 'https://' + process.env.SPACE_HOST : 'http://localhost:' + (process.env.PORT || 7860);
|
| 72 |
-
|
| 73 |
-
cron.schedule('*/5 * * * *', async () => {
|
| 74 |
-
try {
|
| 75 |
-
await fetch(spaceUrl);
|
| 76 |
-
} catch (e) {
|
| 77 |
-
// ignore
|
| 78 |
-
}
|
| 79 |
-
});
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
export function startAutoBackup(): void {
|
| 83 |
-
cron.schedule('*/10 * * * *', async () => {
|
| 84 |
-
await backupAuthSession();
|
| 85 |
-
});
|
| 86 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/idle-monetization.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 💸 IDLE WORKFORCE & MONETIZATION DIRECTIVE (PRIORITY 0001) 🚨
|
| 3 |
-
* NEVER IDLE MANDATE: This A2A sits NEVER simply dumb around.
|
| 4 |
-
* If no task exists, it autonomously seeks out work, bug bounties, surveys, or freelance gigs.
|
| 5 |
-
*/
|
| 6 |
-
|
| 7 |
-
export function startAutonomousIdleLoop() {
|
| 8 |
-
console.log('💸 Autonomous Monetization & Hacking loop enabled (Mega Krass Mode).');
|
| 9 |
-
const runtimeState = globalThis as typeof globalThis & { isAgentBusy?: boolean };
|
| 10 |
-
|
| 11 |
-
// The loop checks every 5 minutes if the agent is idle.
|
| 12 |
-
setInterval(async () => {
|
| 13 |
-
// Skip if currently busy with a user or fleet task
|
| 14 |
-
if (runtimeState.isAgentBusy) {
|
| 15 |
-
return;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
console.log('\n[IDLE-LOOP] Agent has no tasks. Searching for autonomous opportunities...');
|
| 19 |
-
|
| 20 |
-
const tasks = [
|
| 21 |
-
() => console.log(' 🔍 [Bug Bounty] Scanning HackerOne/Bugcrowd/OpenAI for lucrative flaws...'),
|
| 22 |
-
() => console.log(' 🛠️ [Freelancer] Checking AgentWork/ClawGig/Upwork for new jobs (Web/App dev, Moderation)...'),
|
| 23 |
-
() => console.log(' 📋 [Surveys] Completing paid surveys on pre-approved autonomous platforms...'),
|
| 24 |
-
() => console.log(' 🔐 [Hacker Mode] Utilizing webauto-nodriver-mcp and Scrapling for undetected data extraction...')
|
| 25 |
-
];
|
| 26 |
-
|
| 27 |
-
// Pick a random idle task to pretend we are doing something useful
|
| 28 |
-
// In a real implementation, this would trigger actual A2A tasks (e.g. OpenAI completion).
|
| 29 |
-
const randomTask = tasks[Math.floor(Math.random() * tasks.length)];
|
| 30 |
-
randomTask();
|
| 31 |
-
|
| 32 |
-
}, 5 * 60 * 1000); // Every 5 minutes
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/telemetry/ZeroTrustAuth.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { verify } from 'jsonwebtoken';
|
| 2 |
+
const createTemplateLogger = (runId?: string) => ({ failed: async (s: string, e: string, err: any) => console.error(s, e, err), log: async (o: any) => console.log(o), succeeded: async (s: string, e: string, o: any) => console.log(s, e, o) });
|
| 3 |
+
import type { IncomingMessage } from 'node:http';
|
| 4 |
+
|
| 5 |
+
// We fetch the public key (or use a shared secret if configured) from SIN-Authenticator's OIDC jwks endpoint or env
|
| 6 |
+
const OIDC_PUBLIC_KEY = process.env.SIN_OIDC_PUBLIC_KEY || process.env.SIN_SHARED_SECRET || '';
|
| 7 |
+
|
| 8 |
+
export type AuthContext = {
|
| 9 |
+
subject: string;
|
| 10 |
+
roles: string[];
|
| 11 |
+
claims: Record<string, unknown>;
|
| 12 |
+
isValid: boolean;
|
| 13 |
+
error?: string;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export async function verifyZeroTrustToken(request: IncomingMessage, runId?: string): Promise<AuthContext> {
|
| 17 |
+
const logger = createTemplateLogger(runId);
|
| 18 |
+
const authHeader = request.headers.authorization;
|
| 19 |
+
|
| 20 |
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 21 |
+
await logger.failed('auth.zero_trust', 'auth.token.missing', new Error('Missing or malformed Authorization header'));
|
| 22 |
+
return { subject: 'anonymous', roles: [], claims: {}, isValid: false, error: 'missing_token' };
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const token = authHeader.split(' ')[1];
|
| 26 |
+
|
| 27 |
+
if (!OIDC_PUBLIC_KEY) {
|
| 28 |
+
// If the fleet runs in a mode without strict OIDC enforcement configured yet,
|
| 29 |
+
// we log a warning but might fail closed depending on fleet policy.
|
| 30 |
+
// For Epic #195, we want to enforce zero-trust strictly when keys are present.
|
| 31 |
+
await logger.log({
|
| 32 |
+
surface: 'auth.zero_trust',
|
| 33 |
+
event: 'auth.keys.missing',
|
| 34 |
+
level: 'warn',
|
| 35 |
+
status: 'failed',
|
| 36 |
+
details: { note: 'No SIN_OIDC_PUBLIC_KEY or SIN_SHARED_SECRET configured for verification.' }
|
| 37 |
+
});
|
| 38 |
+
return { subject: 'anonymous', roles: [], claims: {}, isValid: false, error: 'missing_keys' };
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
const decoded = verify(token, OIDC_PUBLIC_KEY, { algorithms: ['RS256', 'HS256'] }) as Record<string, unknown>;
|
| 43 |
+
|
| 44 |
+
await logger.succeeded('auth.zero_trust', 'auth.token.verified', { subject: decoded.sub });
|
| 45 |
+
|
| 46 |
+
return {
|
| 47 |
+
subject: String(decoded.sub || 'unknown'),
|
| 48 |
+
roles: Array.isArray(decoded.roles) ? decoded.roles.map(String) : [],
|
| 49 |
+
claims: decoded,
|
| 50 |
+
isValid: true
|
| 51 |
+
};
|
| 52 |
+
} catch (error) {
|
| 53 |
+
await logger.failed('auth.zero_trust', 'auth.token.rejected', error);
|
| 54 |
+
return {
|
| 55 |
+
subject: 'anonymous',
|
| 56 |
+
roles: [],
|
| 57 |
+
claims: {},
|
| 58 |
+
isValid: false,
|
| 59 |
+
error: error instanceof Error ? error.message : String(error)
|
| 60 |
+
};
|
| 61 |
+
}
|
| 62 |
+
}
|
src/mcp-server.ts
CHANGED
|
@@ -17,41 +17,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
| 17 |
inputSchema: { type: 'object', properties: {} },
|
| 18 |
},
|
| 19 |
{
|
| 20 |
-
name: '
|
| 21 |
-
description: '
|
| 22 |
inputSchema: { type: 'object', properties: {} },
|
| 23 |
},
|
| 24 |
{
|
| 25 |
-
name: '
|
| 26 |
-
description: '
|
| 27 |
-
inputSchema: { type: 'object', properties: {} },
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
name: 'sin_github_worker_claim_next',
|
| 31 |
-
description: 'Claim the next Room-13 task for GitHub-Issues.',
|
| 32 |
-
inputSchema: { type: 'object', properties: {} },
|
| 33 |
-
},
|
| 34 |
-
{
|
| 35 |
-
name: 'sin_github_worker_checkpoint',
|
| 36 |
-
description: 'Send a checkpoint to Room-13 for GitHub-Issues.',
|
| 37 |
inputSchema: {
|
| 38 |
type: 'object',
|
| 39 |
properties: {
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
},
|
| 43 |
-
|
| 44 |
-
}
|
| 45 |
},
|
| 46 |
{
|
| 47 |
-
name: '
|
| 48 |
-
description: '
|
| 49 |
inputSchema: {
|
| 50 |
type: 'object',
|
| 51 |
properties: {
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
{
|
| 57 |
name: 'sin_github_project_orchestrate',
|
|
@@ -77,64 +76,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
| 77 |
required: ['prompt'],
|
| 78 |
},
|
| 79 |
},
|
| 80 |
-
{
|
| 81 |
-
name: 'sin_github_issue_ensure',
|
| 82 |
-
description: 'Create or update an issue by exact title match.',
|
| 83 |
-
inputSchema: {
|
| 84 |
-
type: 'object',
|
| 85 |
-
properties: {
|
| 86 |
-
repo: { type: 'string' },
|
| 87 |
-
title: { type: 'string' },
|
| 88 |
-
body: { type: 'string' },
|
| 89 |
-
labels: { type: 'array', items: { type: 'string' } }
|
| 90 |
-
},
|
| 91 |
-
required: ['repo', 'title', 'body']
|
| 92 |
-
}
|
| 93 |
-
},
|
| 94 |
-
{
|
| 95 |
-
name: 'sin_github_issue_comment',
|
| 96 |
-
description: 'Comment on a GitHub issue.',
|
| 97 |
-
inputSchema: {
|
| 98 |
-
type: 'object',
|
| 99 |
-
properties: {
|
| 100 |
-
repo: { type: 'string' },
|
| 101 |
-
issueNumber: { type: 'number' },
|
| 102 |
-
body: { type: 'string' }
|
| 103 |
-
},
|
| 104 |
-
required: ['repo', 'issueNumber', 'body']
|
| 105 |
-
}
|
| 106 |
-
},
|
| 107 |
-
{
|
| 108 |
-
name: 'sin_github_issue_close',
|
| 109 |
-
description: 'Close a GitHub issue with optional comment.',
|
| 110 |
-
inputSchema: {
|
| 111 |
-
type: 'object',
|
| 112 |
-
properties: {
|
| 113 |
-
repo: { type: 'string' },
|
| 114 |
-
issueNumber: { type: 'number' },
|
| 115 |
-
comment: { type: 'string' }
|
| 116 |
-
},
|
| 117 |
-
required: ['repo', 'issueNumber']
|
| 118 |
-
}
|
| 119 |
-
},
|
| 120 |
-
{
|
| 121 |
-
name: 'sin_github_issue_triage_enqueue',
|
| 122 |
-
description: 'Durably enqueue a GitHub issue triage event into the coder dispatch pool.',
|
| 123 |
-
inputSchema: {
|
| 124 |
-
type: 'object',
|
| 125 |
-
properties: {
|
| 126 |
-
repo: { type: 'string' },
|
| 127 |
-
issueNumber: { type: 'number' },
|
| 128 |
-
title: { type: 'string' },
|
| 129 |
-
body: { type: 'string' },
|
| 130 |
-
labels: { type: 'array', items: { type: 'string' } },
|
| 131 |
-
assignedTeam: { type: 'string' },
|
| 132 |
-
deliveryId: { type: 'string' },
|
| 133 |
-
eventAction: { type: 'string' }
|
| 134 |
-
},
|
| 135 |
-
required: ['repo', 'issueNumber', 'title']
|
| 136 |
-
}
|
| 137 |
-
},
|
| 138 |
{
|
| 139 |
name: 'sin_github_wiki_sync',
|
| 140 |
description: 'Update the repository Wiki with project documentation.',
|
|
@@ -195,9 +136,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
| 195 |
required: ['prNumber'],
|
| 196 |
},
|
| 197 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
{
|
| 199 |
name: 'sin_github_ledger_log',
|
| 200 |
-
description: 'Publish an agent activity log or achievement to the public
|
| 201 |
inputSchema: {
|
| 202 |
type: 'object',
|
| 203 |
properties: {
|
|
@@ -217,53 +180,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
| 217 |
let action: GitHubAgentAction;
|
| 218 |
const args = request.params.arguments as any;
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
break;
|
| 224 |
-
case 'sin_github_worker_register':
|
| 225 |
-
action = { action: 'sin.github.worker.register' };
|
| 226 |
-
break;
|
| 227 |
-
case 'sin_github_worker_status':
|
| 228 |
-
action = { action: 'sin.github.worker.status' };
|
| 229 |
-
break;
|
| 230 |
-
case 'sin_github_worker_claim_next':
|
| 231 |
-
action = { action: 'sin.github.worker.claim_next' };
|
| 232 |
-
break;
|
| 233 |
-
case 'sin_github_worker_checkpoint':
|
| 234 |
-
action = { action: 'sin.github.worker.checkpoint', step: args.step, checkpoint: args.checkpoint };
|
| 235 |
-
break;
|
| 236 |
-
case 'sin_github_worker_complete':
|
| 237 |
-
action = { action: 'sin.github.worker.complete', result: args.result };
|
| 238 |
-
break;
|
| 239 |
-
case 'sin_github_project_orchestrate':
|
| 240 |
-
action = { action: 'sin.github.project.orchestrate', prompt: args.prompt, contextDir: args.contextDir };
|
| 241 |
-
break;
|
| 242 |
-
case 'sin_github_issue_manage':
|
| 243 |
-
action = { action: 'sin.github.issue.manage', prompt: args.prompt, issueNumber: args.issueNumber };
|
| 244 |
-
break;
|
| 245 |
-
case 'sin_github_issue_ensure':
|
| 246 |
-
action = { action: 'sin.github.issue.ensure', repo: args.repo, title: args.title, body: args.body, labels: args.labels };
|
| 247 |
break;
|
| 248 |
-
case '
|
| 249 |
-
action = { action: 'sin.github.
|
| 250 |
break;
|
| 251 |
-
case '
|
| 252 |
-
action = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
break;
|
| 254 |
-
case '
|
| 255 |
action = {
|
| 256 |
-
action: 'sin.github.issue.
|
| 257 |
repo: args.repo,
|
| 258 |
issueNumber: args.issueNumber,
|
| 259 |
-
title: args.title,
|
| 260 |
body: args.body,
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
eventAction: args.eventAction
|
| 265 |
};
|
| 266 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
case 'sin_github_wiki_sync':
|
| 268 |
action = { action: 'sin.github.wiki.sync', prompt: args.prompt, contextDir: args.contextDir };
|
| 269 |
break;
|
|
@@ -279,6 +230,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
| 279 |
case 'sin_github_pr_review':
|
| 280 |
action = { action: 'sin.github.pr.review', prNumber: args.prNumber, contextDir: args.contextDir };
|
| 281 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
case 'sin_github_ledger_log':
|
| 283 |
action = { action: 'sin.github.ledger.log', agentName: args.agentName, activityTitle: args.activityTitle, details: args.details };
|
| 284 |
break;
|
|
@@ -301,5 +269,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
| 301 |
export async function runMcpServer() {
|
| 302 |
const transport = new StdioServerTransport();
|
| 303 |
await server.connect(transport);
|
| 304 |
-
console.error('
|
| 305 |
}
|
|
|
|
| 17 |
inputSchema: { type: 'object', properties: {} },
|
| 18 |
},
|
| 19 |
{
|
| 20 |
+
name: 'sin_github_app_routing_status',
|
| 21 |
+
description: 'Inspect per-agent GitHub App routing config without exposing secrets.',
|
| 22 |
inputSchema: { type: 'object', properties: {} },
|
| 23 |
},
|
| 24 |
{
|
| 25 |
+
name: 'sin_github_webhook_route',
|
| 26 |
+
description: 'Resolve a GitHub webhook payload to the correct specialized SIN coder app using installation.app_id and routing config.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
inputSchema: {
|
| 28 |
type: 'object',
|
| 29 |
properties: {
|
| 30 |
+
payload: {},
|
| 31 |
+
rawBody: { type: 'string' },
|
| 32 |
+
routePath: { type: 'string' },
|
| 33 |
+
signature256: { type: 'string' },
|
| 34 |
+
eventName: { type: 'string' },
|
| 35 |
+
deliveryId: { type: 'string' },
|
| 36 |
},
|
| 37 |
+
},
|
|
|
|
| 38 |
},
|
| 39 |
{
|
| 40 |
+
name: 'sin_github_issue_comment_as_app',
|
| 41 |
+
description: 'Post an issue or PR comment as the correct GitHub App identity using a JWT + installation token flow.',
|
| 42 |
inputSchema: {
|
| 43 |
type: 'object',
|
| 44 |
properties: {
|
| 45 |
+
repo: { type: 'string' },
|
| 46 |
+
issueNumber: { type: 'number' },
|
| 47 |
+
body: { type: 'string' },
|
| 48 |
+
agentSlug: { type: 'string' },
|
| 49 |
+
appId: { type: 'number' },
|
| 50 |
+
installationId: { type: 'number' },
|
| 51 |
+
},
|
| 52 |
+
required: ['repo', 'issueNumber', 'body', 'installationId'],
|
| 53 |
+
},
|
| 54 |
},
|
| 55 |
{
|
| 56 |
name: 'sin_github_project_orchestrate',
|
|
|
|
| 76 |
required: ['prompt'],
|
| 77 |
},
|
| 78 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
{
|
| 80 |
name: 'sin_github_wiki_sync',
|
| 81 |
description: 'Update the repository Wiki with project documentation.',
|
|
|
|
| 136 |
required: ['prNumber'],
|
| 137 |
},
|
| 138 |
},
|
| 139 |
+
{
|
| 140 |
+
name: 'sin_github_issue_pool_enqueue',
|
| 141 |
+
description: 'Insert an issue into the Hermes issue pool for Team-Coding dispatch.',
|
| 142 |
+
inputSchema: {
|
| 143 |
+
type: 'object',
|
| 144 |
+
properties: {
|
| 145 |
+
repoName: { type: 'string' },
|
| 146 |
+
issueNumber: { type: 'number' },
|
| 147 |
+
title: { type: 'string' },
|
| 148 |
+
body: { type: 'string' },
|
| 149 |
+
labels: { type: 'array', items: { type: 'string' } },
|
| 150 |
+
issueUrl: { type: 'string' },
|
| 151 |
+
assignedTeam: { type: 'string' },
|
| 152 |
+
assignedAgent: { type: 'string' },
|
| 153 |
+
status: { type: 'string' },
|
| 154 |
+
state: { type: 'string' },
|
| 155 |
+
routingStrategy: { type: 'string' },
|
| 156 |
+
fanoutPlan: { type: 'array', items: {} },
|
| 157 |
+
},
|
| 158 |
+
required: ['repoName', 'issueNumber', 'title'],
|
| 159 |
+
},
|
| 160 |
+
},
|
| 161 |
{
|
| 162 |
name: 'sin_github_ledger_log',
|
| 163 |
+
description: 'Publish an agent activity log or achievement to the public OpenSIN-Ledger showcase repository.',
|
| 164 |
inputSchema: {
|
| 165 |
type: 'object',
|
| 166 |
properties: {
|
|
|
|
| 180 |
let action: GitHubAgentAction;
|
| 181 |
const args = request.params.arguments as any;
|
| 182 |
|
| 183 |
+
switch (request.params.name) {
|
| 184 |
+
case 'sin_github_health':
|
| 185 |
+
action = { action: 'sin.github.health' };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
break;
|
| 187 |
+
case 'sin_github_app_routing_status':
|
| 188 |
+
action = { action: 'sin.github.app.routing.status' };
|
| 189 |
break;
|
| 190 |
+
case 'sin_github_webhook_route':
|
| 191 |
+
action = {
|
| 192 |
+
action: 'sin.github.webhook.route',
|
| 193 |
+
payload: args.payload,
|
| 194 |
+
rawBody: args.rawBody,
|
| 195 |
+
routePath: args.routePath,
|
| 196 |
+
signature256: args.signature256,
|
| 197 |
+
eventName: args.eventName,
|
| 198 |
+
deliveryId: args.deliveryId,
|
| 199 |
+
};
|
| 200 |
break;
|
| 201 |
+
case 'sin_github_issue_comment_as_app':
|
| 202 |
action = {
|
| 203 |
+
action: 'sin.github.issue.comment.as_app',
|
| 204 |
repo: args.repo,
|
| 205 |
issueNumber: args.issueNumber,
|
|
|
|
| 206 |
body: args.body,
|
| 207 |
+
agentSlug: args.agentSlug,
|
| 208 |
+
appId: args.appId,
|
| 209 |
+
installationId: args.installationId,
|
|
|
|
| 210 |
};
|
| 211 |
break;
|
| 212 |
+
case 'sin_github_project_orchestrate':
|
| 213 |
+
action = { action: 'sin.github.project.orchestrate', prompt: args.prompt, contextDir: args.contextDir };
|
| 214 |
+
break;
|
| 215 |
+
case 'sin_github_issue_manage':
|
| 216 |
+
action = { action: 'sin.github.issue.manage', prompt: args.prompt, issueNumber: args.issueNumber };
|
| 217 |
+
break;
|
| 218 |
case 'sin_github_wiki_sync':
|
| 219 |
action = { action: 'sin.github.wiki.sync', prompt: args.prompt, contextDir: args.contextDir };
|
| 220 |
break;
|
|
|
|
| 230 |
case 'sin_github_pr_review':
|
| 231 |
action = { action: 'sin.github.pr.review', prNumber: args.prNumber, contextDir: args.contextDir };
|
| 232 |
break;
|
| 233 |
+
case 'sin_github_issue_pool_enqueue':
|
| 234 |
+
action = {
|
| 235 |
+
action: 'sin.github.issue.pool.enqueue',
|
| 236 |
+
repoName: args.repoName,
|
| 237 |
+
issueNumber: args.issueNumber,
|
| 238 |
+
title: args.title,
|
| 239 |
+
body: args.body,
|
| 240 |
+
labels: args.labels,
|
| 241 |
+
issueUrl: args.issueUrl,
|
| 242 |
+
assignedTeam: args.assignedTeam,
|
| 243 |
+
assignedAgent: args.assignedAgent,
|
| 244 |
+
status: args.status,
|
| 245 |
+
state: args.state,
|
| 246 |
+
routingStrategy: args.routingStrategy,
|
| 247 |
+
fanoutPlan: args.fanoutPlan,
|
| 248 |
+
};
|
| 249 |
+
break;
|
| 250 |
case 'sin_github_ledger_log':
|
| 251 |
action = { action: 'sin.github.ledger.log', agentName: args.agentName, activityTitle: args.activityTitle, details: args.details };
|
| 252 |
break;
|
|
|
|
| 269 |
export async function runMcpServer() {
|
| 270 |
const transport = new StdioServerTransport();
|
| 271 |
await server.connect(transport);
|
| 272 |
+
console.error('A2A-SIN-GitHub-Issues MCP Server running on stdio');
|
| 273 |
}
|
src/metadata.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import process from 'node:process';
|
| 2 |
|
| 3 |
export const TEMPLATE_AGENT_ID = 'sin-github-issues';
|
| 4 |
-
export const TEMPLATE_AGENT_NAME = '
|
| 5 |
-
export const TEMPLATE_AGENT_DESCRIPTION = 'CEO Elite GitHub
|
| 6 |
export const TEMPLATE_AGENT_VERSION = '2026.03.22';
|
| 7 |
export const TEMPLATE_AGENT_DEFAULT_HOST = '127.0.0.1';
|
| 8 |
export const TEMPLATE_AGENT_DEFAULT_PORT = 46031;
|
|
@@ -14,29 +14,19 @@ export const TEMPLATE_AGENT_SKILLS = [
|
|
| 14 |
description: 'Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities.',
|
| 15 |
},
|
| 16 |
{
|
| 17 |
-
id: 'sin.github.
|
| 18 |
-
name: '
|
| 19 |
-
description: '
|
| 20 |
},
|
| 21 |
{
|
| 22 |
-
id: 'sin.github.
|
| 23 |
-
name: '
|
| 24 |
-
description: '
|
| 25 |
},
|
| 26 |
{
|
| 27 |
-
id: 'sin.github.
|
| 28 |
-
name: '
|
| 29 |
-
description: '
|
| 30 |
-
},
|
| 31 |
-
{
|
| 32 |
-
id: 'sin.github.worker.checkpoint',
|
| 33 |
-
name: 'Worker Checkpoint',
|
| 34 |
-
description: 'Send checkpoint progress for the current Room-13 task.',
|
| 35 |
-
},
|
| 36 |
-
{
|
| 37 |
-
id: 'sin.github.worker.complete',
|
| 38 |
-
name: 'Worker Complete',
|
| 39 |
-
description: 'Mark the currently claimed Room-13 task as completed.',
|
| 40 |
},
|
| 41 |
{
|
| 42 |
id: 'sin.github.project.orchestrate',
|
|
@@ -73,10 +63,15 @@ export const TEMPLATE_AGENT_SKILLS = [
|
|
| 73 |
name: 'PR Review',
|
| 74 |
description: 'Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass.',
|
| 75 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
{
|
| 77 |
id: 'sin.github.ledger.log',
|
| 78 |
name: 'Ledger Log',
|
| 79 |
-
description: 'Publish an agent activity log or achievement to the public Delqhi/
|
| 80 |
},
|
| 81 |
] as const;
|
| 82 |
|
|
|
|
| 1 |
import process from 'node:process';
|
| 2 |
|
| 3 |
export const TEMPLATE_AGENT_ID = 'sin-github-issues';
|
| 4 |
+
export const TEMPLATE_AGENT_NAME = 'SIN-GitHub-Issues';
|
| 5 |
+
export const TEMPLATE_AGENT_DESCRIPTION = 'CEO Elite GitHub Operations Architect — orchestrates Projects V2, Issues, Wikis, Discussions, Gists, Security audits, PR Reviews, and public Showcase Ledger logging.';
|
| 6 |
export const TEMPLATE_AGENT_VERSION = '2026.03.22';
|
| 7 |
export const TEMPLATE_AGENT_DEFAULT_HOST = '127.0.0.1';
|
| 8 |
export const TEMPLATE_AGENT_DEFAULT_PORT = 46031;
|
|
|
|
| 14 |
description: 'Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities.',
|
| 15 |
},
|
| 16 |
{
|
| 17 |
+
id: 'sin.github.app.routing.status',
|
| 18 |
+
name: 'GitHub App Routing Status',
|
| 19 |
+
description: 'Inspect the configured multi-app GitHub routing table, route paths, and secret readiness without exposing credentials.',
|
| 20 |
},
|
| 21 |
{
|
| 22 |
+
id: 'sin.github.webhook.route',
|
| 23 |
+
name: 'GitHub Webhook Route',
|
| 24 |
+
description: 'Route GitHub webhook payloads to the correct SIN coder agent using installation.app_id, route path, and signature verification.',
|
| 25 |
},
|
| 26 |
{
|
| 27 |
+
id: 'sin.github.issue.comment.as_app',
|
| 28 |
+
name: 'Issue Comment As App',
|
| 29 |
+
description: 'Generate the correct GitHub App JWT and installation token, then post an issue or PR comment as the matched bot identity.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
},
|
| 31 |
{
|
| 32 |
id: 'sin.github.project.orchestrate',
|
|
|
|
| 63 |
name: 'PR Review',
|
| 64 |
description: 'Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass.',
|
| 65 |
},
|
| 66 |
+
{
|
| 67 |
+
id: 'sin.github.issue.pool.enqueue',
|
| 68 |
+
name: 'Issue Pool Enqueue',
|
| 69 |
+
description: 'Insert GitHub issue metadata into `sin_issues_pool` for Hermes/Team-Coding dispatch.',
|
| 70 |
+
},
|
| 71 |
{
|
| 72 |
id: 'sin.github.ledger.log',
|
| 73 |
name: 'Ledger Log',
|
| 74 |
+
description: 'Publish an agent activity log or achievement to the public Delqhi/OpenSIN-Ledger showcase repository.',
|
| 75 |
},
|
| 76 |
] as const;
|
| 77 |
|
src/queue-state.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
export const ISSUE_QUEUE_STATE = {
|
| 2 |
-
received: 'received',
|
| 3 |
-
queued: 'queued',
|
| 4 |
-
claimed: 'claimed',
|
| 5 |
-
dispatched: 'dispatched',
|
| 6 |
-
accepted: 'accepted',
|
| 7 |
-
completed: 'completed',
|
| 8 |
-
failed: 'failed',
|
| 9 |
-
dead_letter: 'dead_letter',
|
| 10 |
-
};
|
| 11 |
-
|
| 12 |
-
export function isActiveQueueState(value) {
|
| 13 |
-
return ['received', 'queued', 'claimed', 'dispatched', 'accepted'].includes(String(value || ''));
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export function queueStateForStatus(value) {
|
| 17 |
-
switch (String(value || '').toLowerCase()) {
|
| 18 |
-
case 'pending':
|
| 19 |
-
return ISSUE_QUEUE_STATE.queued;
|
| 20 |
-
case 'in_progress':
|
| 21 |
-
return ISSUE_QUEUE_STATE.claimed;
|
| 22 |
-
case 'completed':
|
| 23 |
-
return ISSUE_QUEUE_STATE.completed;
|
| 24 |
-
case 'failed':
|
| 25 |
-
return ISSUE_QUEUE_STATE.failed;
|
| 26 |
-
default:
|
| 27 |
-
return ISSUE_QUEUE_STATE.received;
|
| 28 |
-
}
|
| 29 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/queue-state.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
export const ISSUE_QUEUE_STATE = {
|
| 2 |
-
received: 'received',
|
| 3 |
-
queued: 'queued',
|
| 4 |
-
claimed: 'claimed',
|
| 5 |
-
dispatched: 'dispatched',
|
| 6 |
-
accepted: 'accepted',
|
| 7 |
-
completed: 'completed',
|
| 8 |
-
failed: 'failed',
|
| 9 |
-
dead_letter: 'dead_letter',
|
| 10 |
-
} as const;
|
| 11 |
-
|
| 12 |
-
export type IssueQueueState = keyof typeof ISSUE_QUEUE_STATE;
|
| 13 |
-
|
| 14 |
-
export function isActiveQueueState(value: string | null | undefined): boolean {
|
| 15 |
-
return ['received', 'queued', 'claimed', 'dispatched', 'accepted'].includes(String(value || ''));
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export function queueStateForStatus(status: string | null | undefined): IssueQueueState {
|
| 19 |
-
switch (String(status || '').toLowerCase()) {
|
| 20 |
-
case 'pending':
|
| 21 |
-
return 'queued';
|
| 22 |
-
case 'in_progress':
|
| 23 |
-
return 'claimed';
|
| 24 |
-
case 'completed':
|
| 25 |
-
return 'completed';
|
| 26 |
-
case 'failed':
|
| 27 |
-
return 'failed';
|
| 28 |
-
default:
|
| 29 |
-
return 'received';
|
| 30 |
-
}
|
| 31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/room13-worker.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
| 1 |
-
import os from 'node:os';
|
| 2 |
-
import process from 'node:process';
|
| 3 |
-
|
| 4 |
-
type Room13WorkerRegisterResponse = {
|
| 5 |
-
id?: string;
|
| 6 |
-
lease_epoch?: number;
|
| 7 |
-
};
|
| 8 |
-
|
| 9 |
-
type Room13WorkerClaimResponse = {
|
| 10 |
-
claimed_task_id?: string | null;
|
| 11 |
-
lease_epoch?: number;
|
| 12 |
-
lease_expires_at?: string | null;
|
| 13 |
-
run_id?: string;
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
type Room13Task = {
|
| 17 |
-
id: string;
|
| 18 |
-
status: string;
|
| 19 |
-
assigned_worker?: string | null;
|
| 20 |
-
run_id?: string | null;
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
export type GitHubIssuesWorkerSnapshot = {
|
| 24 |
-
enabled: boolean;
|
| 25 |
-
workerName: string;
|
| 26 |
-
workerId: string;
|
| 27 |
-
runId: string;
|
| 28 |
-
claimedTaskId: string;
|
| 29 |
-
leaseEpoch: number;
|
| 30 |
-
leaseExpiresAt?: string;
|
| 31 |
-
baseUrl: string;
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
let currentRuntime: GitHubIssuesRoom13WorkerRuntime | null = null;
|
| 35 |
-
|
| 36 |
-
export function getCurrentGitHubIssuesWorkerRuntime() {
|
| 37 |
-
return currentRuntime;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
export class GitHubIssuesRoom13WorkerRuntime {
|
| 41 |
-
protected workerId = '';
|
| 42 |
-
protected leaseEpoch = 0;
|
| 43 |
-
protected claimedTaskId = '';
|
| 44 |
-
protected leaseExpiresAt?: string;
|
| 45 |
-
protected readonly workerName: string;
|
| 46 |
-
protected readonly baseUrl: string;
|
| 47 |
-
protected readonly bearerToken: string;
|
| 48 |
-
protected readonly runId: string;
|
| 49 |
-
protected heartbeatTimer?: NodeJS.Timeout;
|
| 50 |
-
|
| 51 |
-
constructor(workerName = 'sin-github-issues', baseUrl = process.env.ROOM13_COORDINATOR_URL || '') {
|
| 52 |
-
this.workerName = workerName;
|
| 53 |
-
this.baseUrl = baseUrl.replace(/\/$/, '');
|
| 54 |
-
this.bearerToken = String(process.env.ROOM13_BEARER_TOKEN || '').trim();
|
| 55 |
-
this.runId = `${workerName}:${process.pid}:${Date.now()}`;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
get enabled() {
|
| 59 |
-
return Boolean(this.baseUrl);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
snapshot(): GitHubIssuesWorkerSnapshot {
|
| 63 |
-
return {
|
| 64 |
-
enabled: this.enabled,
|
| 65 |
-
workerName: this.workerName,
|
| 66 |
-
workerId: this.workerId,
|
| 67 |
-
runId: this.runId,
|
| 68 |
-
claimedTaskId: this.claimedTaskId,
|
| 69 |
-
leaseEpoch: this.leaseEpoch,
|
| 70 |
-
leaseExpiresAt: this.leaseExpiresAt,
|
| 71 |
-
baseUrl: this.baseUrl,
|
| 72 |
-
};
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
async start() {
|
| 76 |
-
if (!this.enabled) return this.snapshot();
|
| 77 |
-
const registered = await this.request<Room13WorkerRegisterResponse>('POST', '/api/workers', {
|
| 78 |
-
name: `${this.workerName}:${process.env.SPACE_HOST || 'local'}`,
|
| 79 |
-
type: 'general',
|
| 80 |
-
capabilities: ['platform:github', 'worker_runtime', 'lease_heartbeat', 'issue_lifecycle'],
|
| 81 |
-
metadata: { runtime: 'node', hostname: os.hostname() },
|
| 82 |
-
worker_identity: this.workerName,
|
| 83 |
-
persona_id: this.workerName,
|
| 84 |
-
platform: 'github-issues',
|
| 85 |
-
current_run_id: this.runId,
|
| 86 |
-
lease_ttl_sec: 120,
|
| 87 |
-
approval_state: 'idle',
|
| 88 |
-
});
|
| 89 |
-
this.workerId = String(registered?.id || '').trim();
|
| 90 |
-
this.leaseEpoch = Number(registered?.lease_epoch || 0);
|
| 91 |
-
if (this.workerId) {
|
| 92 |
-
const claim = await this.request<Room13WorkerClaimResponse>('POST', `/api/workers/${this.workerId}/claim`, {
|
| 93 |
-
run_id: this.runId,
|
| 94 |
-
ttl_sec: 120,
|
| 95 |
-
});
|
| 96 |
-
this.claimedTaskId = String(claim?.claimed_task_id || '').trim();
|
| 97 |
-
this.leaseEpoch = Number(claim?.lease_epoch || this.leaseEpoch || 0);
|
| 98 |
-
this.leaseExpiresAt = claim?.lease_expires_at || undefined;
|
| 99 |
-
this.startHeartbeatLoop();
|
| 100 |
-
}
|
| 101 |
-
return this.snapshot();
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
async stop() {
|
| 105 |
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
| 106 |
-
if (!this.enabled || !this.workerId) return;
|
| 107 |
-
await this.request('POST', `/api/workers/${this.workerId}/release`, {
|
| 108 |
-
lease_epoch: this.leaseEpoch,
|
| 109 |
-
clear_run: true,
|
| 110 |
-
final_status: 'online',
|
| 111 |
-
status_reason: 'github_issues_runtime_stop',
|
| 112 |
-
approval_state: 'idle',
|
| 113 |
-
}).catch(() => undefined);
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
async heartbeat() {
|
| 117 |
-
if (!this.enabled || !this.workerId) return this.snapshot();
|
| 118 |
-
const result = await this.request<Room13WorkerClaimResponse>('POST', `/api/workers/${this.workerId}/heartbeat`, {
|
| 119 |
-
lease_epoch: this.leaseEpoch,
|
| 120 |
-
ttl_sec: 120,
|
| 121 |
-
metadata: {
|
| 122 |
-
run_id: this.runId,
|
| 123 |
-
claimed_task_id: this.claimedTaskId,
|
| 124 |
-
last_heartbeat_source: 'sin-github-issues',
|
| 125 |
-
},
|
| 126 |
-
});
|
| 127 |
-
this.leaseEpoch = Number(result?.lease_epoch || this.leaseEpoch || 0);
|
| 128 |
-
this.leaseExpiresAt = result?.lease_expires_at || this.leaseExpiresAt;
|
| 129 |
-
return this.snapshot();
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
async claimNextTask() {
|
| 133 |
-
if (!this.enabled || !this.workerId) throw new Error('room13_worker_not_registered');
|
| 134 |
-
const task = await this.request<Room13Task>('POST', '/api/tasks/claim-next', {
|
| 135 |
-
worker_id: this.workerId,
|
| 136 |
-
worker_identity: this.workerName,
|
| 137 |
-
accepted_types: ['github_issue_dispatch'],
|
| 138 |
-
min_priority: 'normal',
|
| 139 |
-
run_id: this.runId,
|
| 140 |
-
lease_epoch: this.leaseEpoch,
|
| 141 |
-
});
|
| 142 |
-
this.claimedTaskId = String(task?.id || '').trim();
|
| 143 |
-
return task;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
async checkpoint(step: string, checkpoint: Record<string, unknown> = {}) {
|
| 147 |
-
if (!this.enabled || !this.workerId) throw new Error('room13_worker_not_registered');
|
| 148 |
-
return await this.request('POST', `/api/workers/${this.workerId}/checkpoint`, {
|
| 149 |
-
lease_epoch: this.leaseEpoch,
|
| 150 |
-
run_id: this.runId,
|
| 151 |
-
step,
|
| 152 |
-
checkpoint,
|
| 153 |
-
});
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
async completeTask(result: Record<string, unknown> = {}) {
|
| 157 |
-
if (!this.enabled || !this.claimedTaskId) throw new Error('room13_task_not_claimed');
|
| 158 |
-
const response = await this.request('PUT', `/api/tasks/${this.claimedTaskId}/complete`, result);
|
| 159 |
-
this.claimedTaskId = '';
|
| 160 |
-
return response;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
private startHeartbeatLoop() {
|
| 164 |
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
| 165 |
-
this.heartbeatTimer = setInterval(() => {
|
| 166 |
-
void this.heartbeat().catch(() => undefined);
|
| 167 |
-
}, 30000);
|
| 168 |
-
this.heartbeatTimer.unref();
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
private async request<T = unknown>(method: string, path: string, body?: unknown) {
|
| 172 |
-
const headers: Record<string, string> = {
|
| 173 |
-
'content-type': 'application/json',
|
| 174 |
-
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
| 175 |
-
};
|
| 176 |
-
if (this.bearerToken) headers.Authorization = `Bearer ${this.bearerToken}`;
|
| 177 |
-
const response = await fetch(`${this.baseUrl}${path}`, {
|
| 178 |
-
method,
|
| 179 |
-
headers,
|
| 180 |
-
body: body ? JSON.stringify(body) : undefined,
|
| 181 |
-
});
|
| 182 |
-
const text = await response.text();
|
| 183 |
-
const parsed = text ? JSON.parse(text) : {};
|
| 184 |
-
if (!response.ok) throw new Error(`room13_http_${response.status}:${JSON.stringify(parsed).slice(0, 1000)}`);
|
| 185 |
-
return parsed as T;
|
| 186 |
-
}
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
export function installGitHubIssuesWorkerRuntime(runtime: GitHubIssuesRoom13WorkerRuntime | null) {
|
| 190 |
-
currentRuntime = runtime;
|
| 191 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/runtime.ts
CHANGED
|
@@ -1,42 +1,20 @@
|
|
| 1 |
-
import { exec } from 'node:child_process';
|
|
|
|
|
|
|
|
|
|
| 2 |
import { promisify } from 'node:util';
|
| 3 |
-
import { commentIssue, closeIssue, ensureIssue } from './github.js';
|
| 4 |
import { createClient } from '@supabase/supabase-js';
|
| 5 |
-
import {
|
| 6 |
-
import {
|
| 7 |
-
buildDeliveryKey,
|
| 8 |
-
buildIssueKey,
|
| 9 |
-
routeIssueTriage,
|
| 10 |
-
routeLabelsToTeam,
|
| 11 |
-
TriageEnqueueSchema,
|
| 12 |
-
type TriageEnqueueInput,
|
| 13 |
-
} from './triage.js';
|
| 14 |
-
import { ISSUE_QUEUE_STATE } from './queue-state.js';
|
| 15 |
|
| 16 |
const execAsync = promisify(exec);
|
|
|
|
| 17 |
|
| 18 |
export type GitHubAgentAction =
|
| 19 |
| { action: 'agent.help' }
|
| 20 |
| { action: 'sin.github.health' }
|
| 21 |
-
| { action: 'sin.github.
|
| 22 |
-
| { action: 'sin.github.
|
| 23 |
-
| { action: 'sin.github.
|
| 24 |
-
| { action: 'sin.github.worker.checkpoint'; step: string; checkpoint?: Record<string, unknown> }
|
| 25 |
-
| { action: 'sin.github.worker.complete'; result?: Record<string, unknown> }
|
| 26 |
-
| { action: 'sin.github.issue.ensure'; repo: string; title: string; body: string; labels?: string[] }
|
| 27 |
-
| { action: 'sin.github.issue.comment'; repo: string; issueNumber: number; body: string }
|
| 28 |
-
| { action: 'sin.github.issue.close'; repo: string; issueNumber: number; comment?: string }
|
| 29 |
-
| {
|
| 30 |
-
action: 'sin.github.issue.triage.enqueue';
|
| 31 |
-
repo: string;
|
| 32 |
-
issueNumber: number;
|
| 33 |
-
title: string;
|
| 34 |
-
body?: string;
|
| 35 |
-
labels?: string[];
|
| 36 |
-
assignedTeam?: string;
|
| 37 |
-
deliveryId?: string;
|
| 38 |
-
eventAction?: string;
|
| 39 |
-
}
|
| 40 |
| { action: 'sin.github.project.orchestrate'; prompt: string; contextDir?: string }
|
| 41 |
| { action: 'sin.github.issue.manage'; prompt: string; issueNumber?: number }
|
| 42 |
| { action: 'sin.github.wiki.sync'; prompt: string; contextDir?: string }
|
|
@@ -44,6 +22,7 @@ export type GitHubAgentAction =
|
|
| 44 |
| { action: 'sin.github.gist.publish'; prompt: string; isPublic?: boolean }
|
| 45 |
| { action: 'sin.github.security.audit'; prompt: string; contextDir?: string }
|
| 46 |
| { action: 'sin.github.pr.review'; prNumber: number; contextDir?: string }
|
|
|
|
| 47 |
| { action: 'sin.github.ledger.log'; agentName: string; activityTitle: string; details: string };
|
| 48 |
|
| 49 |
export async function executeGitHubAgentAction(action: GitHubAgentAction): Promise<unknown> {
|
|
@@ -55,15 +34,9 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
|
|
| 55 |
mandate: 'CEO Elite GitHub Operations. Orchestrates Projects, Issues, Wikis, Discussions, Gists, Security, PR Reviews, and public Showcase Ledgers.',
|
| 56 |
actions: [
|
| 57 |
'sin.github.health',
|
| 58 |
-
'sin.github.
|
| 59 |
-
'sin.github.
|
| 60 |
-
'sin.github.
|
| 61 |
-
'sin.github.worker.checkpoint',
|
| 62 |
-
'sin.github.worker.complete',
|
| 63 |
-
'sin.github.issue.ensure',
|
| 64 |
-
'sin.github.issue.comment',
|
| 65 |
-
'sin.github.issue.close',
|
| 66 |
-
'sin.github.issue.triage.enqueue',
|
| 67 |
'sin.github.project.orchestrate',
|
| 68 |
'sin.github.issue.manage',
|
| 69 |
'sin.github.wiki.sync',
|
|
@@ -71,6 +44,7 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
|
|
| 71 |
'sin.github.gist.publish',
|
| 72 |
'sin.github.security.audit',
|
| 73 |
'sin.github.pr.review',
|
|
|
|
| 74 |
'sin.github.ledger.log'
|
| 75 |
],
|
| 76 |
};
|
|
@@ -83,41 +57,18 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
|
|
| 83 |
fallbackModel: 'opencode/minimax-m2.5-free',
|
| 84 |
team: 'Team - Coding',
|
| 85 |
status: 'Elite 2026 GitHub Operations Architect Online',
|
| 86 |
-
capabilities: ['projects', 'wikis', 'discussions', 'gists', 'security', 'prs', 'issues', 'ledger'],
|
| 87 |
-
|
| 88 |
};
|
| 89 |
|
| 90 |
-
case 'sin.github.
|
| 91 |
-
return
|
| 92 |
|
| 93 |
-
case 'sin.github.
|
| 94 |
-
return
|
| 95 |
|
| 96 |
-
case 'sin.github.
|
| 97 |
-
return
|
| 98 |
-
|
| 99 |
-
case 'sin.github.worker.checkpoint':
|
| 100 |
-
return { ok: true, checkpoint: await getCurrentGitHubIssuesWorkerRuntime()?.checkpoint(action.step, action.checkpoint || {}) };
|
| 101 |
-
|
| 102 |
-
case 'sin.github.worker.complete':
|
| 103 |
-
return { ok: true, completion: await getCurrentGitHubIssuesWorkerRuntime()?.completeTask(action.result || {}) };
|
| 104 |
-
|
| 105 |
-
case 'sin.github.issue.ensure':
|
| 106 |
-
return await ensureIssue({
|
| 107 |
-
repo: action.repo,
|
| 108 |
-
title: action.title,
|
| 109 |
-
body: action.body,
|
| 110 |
-
labels: action.labels,
|
| 111 |
-
});
|
| 112 |
-
|
| 113 |
-
case 'sin.github.issue.comment':
|
| 114 |
-
return await commentIssue(action.repo, action.issueNumber, action.body);
|
| 115 |
-
|
| 116 |
-
case 'sin.github.issue.close':
|
| 117 |
-
return await closeIssue(action.repo, action.issueNumber, action.comment);
|
| 118 |
-
|
| 119 |
-
case 'sin.github.issue.triage.enqueue':
|
| 120 |
-
return await enqueueIssueTriage(action);
|
| 121 |
|
| 122 |
case 'sin.github.project.orchestrate':
|
| 123 |
return await executeOpenCode(
|
|
@@ -158,21 +109,20 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
|
|
| 158 |
action.contextDir
|
| 159 |
);
|
| 160 |
|
|
|
|
|
|
|
|
|
|
| 161 |
case 'sin.github.ledger.log':
|
| 162 |
-
return await
|
| 163 |
-
`Create a new public GitHub Issue or Discussion in the repository 'Delqhi/OpenSolver-Ledger' titled "[${action.agentName}] ${action.activityTitle}". The body must contain the following details formatted in premium markdown to showcase our fleet's capabilities: \n\n${action.details}`
|
| 164 |
-
);
|
| 165 |
}
|
| 166 |
}
|
| 167 |
|
| 168 |
async function executeOpenCode(prompt: string, dir?: string) {
|
| 169 |
await execAsync('python3 scripts/hf_pull_script.py').catch(() => console.warn('Warning: hf_pull_script failed. Proceeding with cached credentials.'));
|
| 170 |
-
|
| 171 |
-
const cmd = `opencode run "${prompt.replace(/"/g, '\\"')}" --model openai/gpt-5.4 --fallback opencode/minimax-m2.5-free`;
|
| 172 |
const options = dir ? { cwd: dir } : {};
|
| 173 |
|
| 174 |
try {
|
| 175 |
-
const { stdout, stderr } = await
|
| 176 |
return {
|
| 177 |
ok: true,
|
| 178 |
expertAnalysis: stdout,
|
|
@@ -183,129 +133,96 @@ async function executeOpenCode(prompt: string, dir?: string) {
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
|
| 186 |
-
async function
|
| 187 |
-
const
|
| 188 |
-
const
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
if (deliveryError) throw deliveryError;
|
| 207 |
-
|
| 208 |
-
if (byDelivery) {
|
| 209 |
return {
|
| 210 |
ok: true,
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
status: byDelivery.status,
|
| 216 |
-
deliveryId,
|
| 217 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
|
|
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
.eq('repo_name', input.repo)
|
| 224 |
-
.eq('issue_number', input.issueNumber)
|
| 225 |
-
.maybeSingle();
|
| 226 |
-
if (selectError) throw selectError;
|
| 227 |
-
|
| 228 |
-
if (existing) {
|
| 229 |
-
const mergedMetadata = {
|
| 230 |
-
...(existing.metadata || {}),
|
| 231 |
-
last_delivery_id: deliveryId,
|
| 232 |
-
issue_key: issueKey,
|
| 233 |
-
event_action: input.eventAction || 'opened',
|
| 234 |
-
labels,
|
| 235 |
-
ingress: 'github-issues-runtime',
|
| 236 |
-
route,
|
| 237 |
-
};
|
| 238 |
-
|
| 239 |
-
const { error: updateError } = await supabase
|
| 240 |
-
.from('sin_issues_pool')
|
| 241 |
-
.update({
|
| 242 |
-
metadata: mergedMetadata,
|
| 243 |
-
delivery_id: deliveryId,
|
| 244 |
-
updated_at: new Date().toISOString(),
|
| 245 |
-
})
|
| 246 |
-
.eq('id', existing.id);
|
| 247 |
-
if (updateError) throw updateError;
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
created: false,
|
| 252 |
-
deduplicatedBy: 'issue_key',
|
| 253 |
-
queueId: existing.id,
|
| 254 |
-
issueKey,
|
| 255 |
-
status: existing.status,
|
| 256 |
-
deliveryId,
|
| 257 |
-
};
|
| 258 |
}
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
const payload = {
|
| 261 |
-
issue_number:
|
| 262 |
-
repo_name:
|
| 263 |
-
title:
|
| 264 |
-
body:
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
delivery_id: deliveryId,
|
| 275 |
-
issue_key: issueKey,
|
| 276 |
-
event_action: input.eventAction || 'opened',
|
| 277 |
-
labels,
|
| 278 |
-
ingress: 'github-issues-runtime',
|
| 279 |
-
route,
|
| 280 |
-
},
|
| 281 |
};
|
| 282 |
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
);
|
| 302 |
|
| 303 |
return {
|
| 304 |
ok: true,
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
issueKey,
|
| 308 |
-
status: inserted.status,
|
| 309 |
-
deliveryId,
|
| 310 |
};
|
| 311 |
}
|
|
|
|
| 1 |
+
import { exec, execFile } from 'node:child_process';
|
| 2 |
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
| 3 |
+
import { tmpdir } from 'node:os';
|
| 4 |
+
import { join } from 'node:path';
|
| 5 |
import { promisify } from 'node:util';
|
|
|
|
| 6 |
import { createClient } from '@supabase/supabase-js';
|
| 7 |
+
import { commentIssueAsGitHubApp, getGitHubAppRoutingStatus, routeGitHubWebhook } from './github-app-routing.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
const execAsync = promisify(exec);
|
| 10 |
+
const execFileAsync = promisify(execFile);
|
| 11 |
|
| 12 |
export type GitHubAgentAction =
|
| 13 |
| { action: 'agent.help' }
|
| 14 |
| { action: 'sin.github.health' }
|
| 15 |
+
| { action: 'sin.github.app.routing.status' }
|
| 16 |
+
| { action: 'sin.github.webhook.route'; payload?: unknown; rawBody?: string; routePath?: string; signature256?: string; eventName?: string; deliveryId?: string }
|
| 17 |
+
| { action: 'sin.github.issue.comment.as_app'; repo: string; issueNumber: number; body: string; agentSlug?: string; appId?: number; installationId: number }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
| { action: 'sin.github.project.orchestrate'; prompt: string; contextDir?: string }
|
| 19 |
| { action: 'sin.github.issue.manage'; prompt: string; issueNumber?: number }
|
| 20 |
| { action: 'sin.github.wiki.sync'; prompt: string; contextDir?: string }
|
|
|
|
| 22 |
| { action: 'sin.github.gist.publish'; prompt: string; isPublic?: boolean }
|
| 23 |
| { action: 'sin.github.security.audit'; prompt: string; contextDir?: string }
|
| 24 |
| { action: 'sin.github.pr.review'; prNumber: number; contextDir?: string }
|
| 25 |
+
| { action: 'sin.github.issue.pool.enqueue'; repoName: string; issueNumber: number; title: string; body?: string; labels?: string[]; issueUrl?: string; assignedTeam?: string; assignedAgent?: string; status?: string; state?: string; routingStrategy?: string; fanoutPlan?: unknown[] }
|
| 26 |
| { action: 'sin.github.ledger.log'; agentName: string; activityTitle: string; details: string };
|
| 27 |
|
| 28 |
export async function executeGitHubAgentAction(action: GitHubAgentAction): Promise<unknown> {
|
|
|
|
| 34 |
mandate: 'CEO Elite GitHub Operations. Orchestrates Projects, Issues, Wikis, Discussions, Gists, Security, PR Reviews, and public Showcase Ledgers.',
|
| 35 |
actions: [
|
| 36 |
'sin.github.health',
|
| 37 |
+
'sin.github.app.routing.status',
|
| 38 |
+
'sin.github.webhook.route',
|
| 39 |
+
'sin.github.issue.comment.as_app',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
'sin.github.project.orchestrate',
|
| 41 |
'sin.github.issue.manage',
|
| 42 |
'sin.github.wiki.sync',
|
|
|
|
| 44 |
'sin.github.gist.publish',
|
| 45 |
'sin.github.security.audit',
|
| 46 |
'sin.github.pr.review',
|
| 47 |
+
'sin.github.issue.pool.enqueue',
|
| 48 |
'sin.github.ledger.log'
|
| 49 |
],
|
| 50 |
};
|
|
|
|
| 57 |
fallbackModel: 'opencode/minimax-m2.5-free',
|
| 58 |
team: 'Team - Coding',
|
| 59 |
status: 'Elite 2026 GitHub Operations Architect Online',
|
| 60 |
+
capabilities: ['projects', 'wikis', 'discussions', 'gists', 'security', 'prs', 'issues', 'ledger', 'github-app-routing'],
|
| 61 |
+
githubAppRouting: await getGitHubAppRoutingStatus(),
|
| 62 |
};
|
| 63 |
|
| 64 |
+
case 'sin.github.app.routing.status':
|
| 65 |
+
return await getGitHubAppRoutingStatus();
|
| 66 |
|
| 67 |
+
case 'sin.github.webhook.route':
|
| 68 |
+
return await routeGitHubWebhook(action);
|
| 69 |
|
| 70 |
+
case 'sin.github.issue.comment.as_app':
|
| 71 |
+
return await commentIssueAsGitHubApp(action);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
case 'sin.github.project.orchestrate':
|
| 74 |
return await executeOpenCode(
|
|
|
|
| 109 |
action.contextDir
|
| 110 |
);
|
| 111 |
|
| 112 |
+
case 'sin.github.issue.pool.enqueue':
|
| 113 |
+
return await enqueueIssuePoolItem(action);
|
| 114 |
+
|
| 115 |
case 'sin.github.ledger.log':
|
| 116 |
+
return await publishLedgerLog(action.agentName, action.activityTitle, action.details);
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
}
|
| 119 |
|
| 120 |
async function executeOpenCode(prompt: string, dir?: string) {
|
| 121 |
await execAsync('python3 scripts/hf_pull_script.py').catch(() => console.warn('Warning: hf_pull_script failed. Proceeding with cached credentials.'));
|
|
|
|
|
|
|
| 122 |
const options = dir ? { cwd: dir } : {};
|
| 123 |
|
| 124 |
try {
|
| 125 |
+
const { stdout, stderr } = await execFileAsync('opencode', ['run', prompt, '--model', 'openai/gpt-5.4'], options);
|
| 126 |
return {
|
| 127 |
ok: true,
|
| 128 |
expertAnalysis: stdout,
|
|
|
|
| 133 |
}
|
| 134 |
}
|
| 135 |
|
| 136 |
+
async function publishLedgerLog(agentName: string, activityTitle: string, details: string) {
|
| 137 |
+
const title = `[${agentName}] ${activityTitle}`;
|
| 138 |
+
const body = [
|
| 139 |
+
`# ${activityTitle}`,
|
| 140 |
+
'',
|
| 141 |
+
`- Agent: ${agentName}`,
|
| 142 |
+
`- Published: ${new Date().toISOString()}`,
|
| 143 |
+
'',
|
| 144 |
+
'## Details',
|
| 145 |
+
details,
|
| 146 |
+
].join('\n');
|
| 147 |
+
const tempDir = await mkdtemp(join(tmpdir(), 'sin-github-ledger-'));
|
| 148 |
+
const bodyPath = join(tempDir, 'ledger-body.md');
|
| 149 |
|
| 150 |
+
try {
|
| 151 |
+
await writeFile(bodyPath, body, 'utf8');
|
| 152 |
+
const { stdout, stderr } = await execFileAsync('gh', [
|
| 153 |
+
'issue',
|
| 154 |
+
'create',
|
| 155 |
+
'-R',
|
| 156 |
+
'Delqhi/OpenSIN-Ledger',
|
| 157 |
+
'--title',
|
| 158 |
+
title,
|
| 159 |
+
'--body-file',
|
| 160 |
+
bodyPath,
|
| 161 |
+
]);
|
|
|
|
|
|
|
|
|
|
| 162 |
return {
|
| 163 |
ok: true,
|
| 164 |
+
repo: 'Delqhi/OpenSIN-Ledger',
|
| 165 |
+
title,
|
| 166 |
+
url: stdout.trim(),
|
| 167 |
+
warnings: stderr || undefined,
|
|
|
|
|
|
|
| 168 |
};
|
| 169 |
+
} catch (error: any) {
|
| 170 |
+
throw new Error(`GitHub Ledger Publish Failed: ${error.message}`);
|
| 171 |
+
} finally {
|
| 172 |
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
| 173 |
}
|
| 174 |
+
}
|
| 175 |
|
| 176 |
+
async function enqueueIssuePoolItem(action: Extract<GitHubAgentAction, { action: 'sin.github.issue.pool.enqueue' }>) {
|
| 177 |
+
const supabaseUrl = (process.env.SUPABASE_URL || process.env.SIN_SUPABASE_URL || '').trim();
|
| 178 |
+
const serviceRoleKey = (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SIN_SUPABASE_SERVICE_ROLE_KEY || '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
+
if (!supabaseUrl || !serviceRoleKey) {
|
| 181 |
+
throw new Error('sin_supabase_config_missing: set SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY or SIN_SUPABASE_URL/SIN_SUPABASE_SERVICE_ROLE_KEY before enqueueing issue-pool items');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
+
const supabase = createClient(supabaseUrl, serviceRoleKey, {
|
| 185 |
+
auth: { persistSession: false, autoRefreshToken: false },
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
const payload = {
|
| 189 |
+
issue_number: action.issueNumber,
|
| 190 |
+
repo_name: action.repoName,
|
| 191 |
+
title: action.title,
|
| 192 |
+
body: action.body || null,
|
| 193 |
+
labels: action.labels || [],
|
| 194 |
+
issue_url: action.issueUrl || null,
|
| 195 |
+
state: action.state || 'open',
|
| 196 |
+
assigned_team: action.assignedTeam || 'team-coding',
|
| 197 |
+
assigned_agent: action.assignedAgent || null,
|
| 198 |
+
status: action.status || 'open',
|
| 199 |
+
routing_strategy: action.routingStrategy || 'single-dispatch',
|
| 200 |
+
fanout_plan: action.fanoutPlan || [],
|
| 201 |
+
updated_at: new Date().toISOString(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
};
|
| 203 |
|
| 204 |
+
let { data, error } = await supabase.from('sin_issues_pool').insert(payload).select('*').single();
|
| 205 |
+
if (error && /column/i.test(error.message)) {
|
| 206 |
+
const fallbackPayload = {
|
| 207 |
+
issue_number: action.issueNumber,
|
| 208 |
+
repo_name: action.repoName,
|
| 209 |
+
title: action.title,
|
| 210 |
+
body: action.body || null,
|
| 211 |
+
state: action.state || 'open',
|
| 212 |
+
assigned_team: action.assignedTeam || 'team-coding',
|
| 213 |
+
assigned_agent: action.assignedAgent || null,
|
| 214 |
+
status: action.status || 'pending',
|
| 215 |
+
updated_at: new Date().toISOString(),
|
| 216 |
+
};
|
| 217 |
+
({ data, error } = await supabase.from('sin_issues_pool').insert(fallbackPayload).select('*').single());
|
| 218 |
+
}
|
| 219 |
+
if (error) {
|
| 220 |
+
throw new Error(`sin_issue_pool_enqueue_failed: ${error.message}`);
|
| 221 |
+
}
|
|
|
|
| 222 |
|
| 223 |
return {
|
| 224 |
ok: true,
|
| 225 |
+
table: 'sin_issues_pool',
|
| 226 |
+
item: data,
|
|
|
|
|
|
|
|
|
|
| 227 |
};
|
| 228 |
}
|
src/self-healing.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import { execFileSync } from 'node:child_process';
|
| 2 |
-
|
| 3 |
-
/**
|
| 4 |
-
* 🚨 GLOBAL FLEET SELF-HEALING PROTOCOL (PRIORITY 0000) 🚨
|
| 5 |
-
* NO-SILO MANDATE: This module catches critical errors, dumps extensive logs,
|
| 6 |
-
* and blasts them to the fleet self-healing webhook.
|
| 7 |
-
* Hermes and AZA-SIN-Coder-•-GitHub-Issues will automatically take over from there.
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
export function triggerFleetSelfHealing(error: Error, extensiveContext: Record<string, any>) {
|
| 11 |
-
console.error('\n🚨 CRITICAL FAILURE DETECTED. INITIATING NO-SILO SELF-HEALING PROTOCOL 🚨');
|
| 12 |
-
|
| 13 |
-
const payload = {
|
| 14 |
-
agentId: 'AZA-SIN-Coder-•-GitHub-Issues',
|
| 15 |
-
timestamp: new Date().toISOString(),
|
| 16 |
-
errorLogs: `Error: ${error.message}\nStack: ${error.stack}\nContext: ${JSON.stringify(extensiveContext, null, 2)}`,
|
| 17 |
-
team: 'team-coding'
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
try {
|
| 21 |
-
const webhookUrl = process.env.FLEET_SELF_HEALING_WEBHOOK || 'http://92.5.60.87:5678/webhook/self-healing';
|
| 22 |
-
|
| 23 |
-
// Blast to the N8N foundation webhook
|
| 24 |
-
const curlCmd = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json" -d '${JSON.stringify(payload).replace(/'/g, "'\\''")}'`;
|
| 25 |
-
|
| 26 |
-
execFileSync('bash', ['-c', curlCmd], { encoding: 'utf8' });
|
| 27 |
-
console.log('✅ Extensive logs successfully transmitted to Fleet Self-Healing pipeline.');
|
| 28 |
-
console.log('👷 The Elite Coder Fleet has been notified and will resolve this architecture flaw autonomously.');
|
| 29 |
-
} catch (transmitError: any) {
|
| 30 |
-
console.error('❌ FATAL: Could not transmit logs to Fleet Self-Healing pipeline.', transmitError.message);
|
| 31 |
-
}
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/triage.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
import { z } from 'zod';
|
| 2 |
-
|
| 3 |
-
export { ISSUE_QUEUE_STATE } from './queue-state.js';
|
| 4 |
-
export type { IssueQueueState as IssueQueueStatus } from './queue-state.js';
|
| 5 |
-
|
| 6 |
-
export const TriageEnqueueSchema = z.object({
|
| 7 |
-
action: z.literal('sin.github.issue.triage.enqueue'),
|
| 8 |
-
repo: z.string().min(3),
|
| 9 |
-
issueNumber: z.number().int().positive(),
|
| 10 |
-
title: z.string().min(1),
|
| 11 |
-
body: z.string().optional(),
|
| 12 |
-
labels: z.array(z.string()).optional(),
|
| 13 |
-
assignedTeam: z.string().optional(),
|
| 14 |
-
deliveryId: z.string().optional(),
|
| 15 |
-
eventAction: z.string().optional(),
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
export type TriageEnqueueInput = z.infer<typeof TriageEnqueueSchema>;
|
| 19 |
-
|
| 20 |
-
export type TriageRoute = {
|
| 21 |
-
assignedTeam: string;
|
| 22 |
-
assignedAgentHint: string;
|
| 23 |
-
capabilityHint: string;
|
| 24 |
-
rationale: string;
|
| 25 |
-
};
|
| 26 |
-
|
| 27 |
-
export function buildIssueKey(repo: string, issueNumber: number) {
|
| 28 |
-
return `${repo}#${issueNumber}`;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
export function buildDeliveryKey(input: Pick<TriageEnqueueInput, 'repo' | 'issueNumber' | 'deliveryId' | 'eventAction'>) {
|
| 32 |
-
const eventAction = (input.eventAction || 'opened').trim() || 'opened';
|
| 33 |
-
const base = `${input.repo}#${input.issueNumber}:${eventAction}`;
|
| 34 |
-
return (input.deliveryId || '').trim() || `manual:${base}`;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
export function routeIssueTriage(labels: string[], title = '', body = ''): TriageRoute {
|
| 38 |
-
const normalized = labels.map((label) => label.toLowerCase());
|
| 39 |
-
const haystack = `${title} ${body}`.toLowerCase();
|
| 40 |
-
|
| 41 |
-
if (normalized.some((label) => label.includes('frontend') || label.includes('ui')) || hasAny(haystack, ['react', 'tsx', 'css', 'layout', 'component'])) {
|
| 42 |
-
return {
|
| 43 |
-
assignedTeam: 'team-coding',
|
| 44 |
-
assignedAgentHint: 'A2A-SIN-Coder-Frontend',
|
| 45 |
-
capabilityHint: 'command:render-ui',
|
| 46 |
-
rationale: 'frontend/ui-signal',
|
| 47 |
-
};
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
if (normalized.some((label) => label.includes('database') || label.includes('sql') || label.includes('supabase')) || hasAny(haystack, ['schema', 'migration', 'postgres', 'rls', 'supabase'])) {
|
| 51 |
-
return {
|
| 52 |
-
assignedTeam: 'team-coding',
|
| 53 |
-
assignedAgentHint: 'A2A-SIN-Coder-Code-Database',
|
| 54 |
-
capabilityHint: 'command:data-model',
|
| 55 |
-
rationale: 'database-signal',
|
| 56 |
-
};
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
if (normalized.some((label) => label.includes('integration') || label.includes('webhook') || label.includes('api')) || hasAny(haystack, ['webhook', 'api', 'integration', 'queue', 'dispatch'])) {
|
| 60 |
-
return {
|
| 61 |
-
assignedTeam: 'team-coding',
|
| 62 |
-
assignedAgentHint: 'A2A-SIN-Coder-Code-Integration',
|
| 63 |
-
capabilityHint: 'command:integration',
|
| 64 |
-
rationale: 'integration-signal',
|
| 65 |
-
};
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
if (normalized.some((label) => label.includes('ai') || label.includes('llm') || label.includes('mcp')) || hasAny(haystack, ['prompt', 'llm', 'agent', 'mcp', 'rag'])) {
|
| 69 |
-
return {
|
| 70 |
-
assignedTeam: 'team-coding',
|
| 71 |
-
assignedAgentHint: 'A2A-SIN-Coder-Code-AI',
|
| 72 |
-
capabilityHint: 'command:ai-runtime',
|
| 73 |
-
rationale: 'ai-signal',
|
| 74 |
-
};
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
if (normalized.some((label) => label.includes('backend') || label.includes('infra') || label.includes('ops')) || hasAny(haystack, ['backend', 'worker', 'lease', 'heartbeat', 'runtime'])) {
|
| 78 |
-
return {
|
| 79 |
-
assignedTeam: 'team-coding',
|
| 80 |
-
assignedAgentHint: 'A2A-SIN-Coder-Backend',
|
| 81 |
-
capabilityHint: 'command:backend-runtime',
|
| 82 |
-
rationale: 'backend-signal',
|
| 83 |
-
};
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
return {
|
| 87 |
-
assignedTeam: 'team-coding',
|
| 88 |
-
assignedAgentHint: 'A2A-SIN-Coder-Backend',
|
| 89 |
-
capabilityHint: 'command:implement',
|
| 90 |
-
rationale: 'default-coder-route',
|
| 91 |
-
};
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
export function routeLabelsToTeam(labels: string[], title = '', body = '') {
|
| 95 |
-
return routeIssueTriage(labels, title, body).assignedTeam;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
function hasAny(haystack: string, needles: string[]) {
|
| 99 |
-
return needles.some((needle) => haystack.includes(needle));
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test/github_workflow.test.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from 'node:assert/strict';
|
| 2 |
+
import { createHmac, generateKeyPairSync } from 'node:crypto';
|
| 3 |
+
import { createServer } from 'node:http';
|
| 4 |
+
import test from 'node:test';
|
| 5 |
+
|
| 6 |
+
const DIST_ROOT = new URL('../dist/src/', import.meta.url);
|
| 7 |
+
|
| 8 |
+
async function importFresh(relativePath) {
|
| 9 |
+
return import(new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, DIST_ROOT).href);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
function setEnv(updates) {
|
| 13 |
+
const previous = new Map();
|
| 14 |
+
for (const [key, value] of Object.entries(updates)) {
|
| 15 |
+
previous.set(key, process.env[key]);
|
| 16 |
+
if (value === undefined || value === null) delete process.env[key];
|
| 17 |
+
else process.env[key] = value;
|
| 18 |
+
}
|
| 19 |
+
return () => {
|
| 20 |
+
for (const [key, value] of previous.entries()) {
|
| 21 |
+
if (value === undefined) delete process.env[key];
|
| 22 |
+
else process.env[key] = value;
|
| 23 |
+
}
|
| 24 |
+
};
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
test('GitHub Agent help exposes routing actions', async () => {
|
| 28 |
+
const { executeGitHubAgentAction } = await importFresh('./runtime.js');
|
| 29 |
+
|
| 30 |
+
const help = await executeGitHubAgentAction({ action: 'agent.help' });
|
| 31 |
+
assert.equal(help.ok, true);
|
| 32 |
+
assert.ok(help.actions.includes('sin.github.app.routing.status'));
|
| 33 |
+
assert.ok(help.actions.includes('sin.github.webhook.route'));
|
| 34 |
+
assert.ok(help.actions.includes('sin.github.issue.comment.as_app'));
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
test('routeGitHubWebhook prefers installation.app_id over shared central path', async () => {
|
| 38 |
+
const restoreEnv = setEnv({
|
| 39 |
+
SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({
|
| 40 |
+
mode: 'central-endpoint-routing',
|
| 41 |
+
defaultWebhookPath: '/api/webhooks/github-apps',
|
| 42 |
+
apps: [
|
| 43 |
+
{
|
| 44 |
+
appIdEnv: 'SIN_HERMES_GITHUB_APP_ID',
|
| 45 |
+
appSlug: 'sin-hermes',
|
| 46 |
+
preferredAgentSlug: 'sin-hermesbote',
|
| 47 |
+
botName: 'SIN-Hermes[bot]',
|
| 48 |
+
webhookPath: '/api/webhooks/github-apps',
|
| 49 |
+
webhookSecretEnv: 'SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET',
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
appIdEnv: 'SIN_BUGBOUNTY_GITHUB_APP_ID',
|
| 53 |
+
appSlug: 'sin-bugbounty',
|
| 54 |
+
preferredAgentSlug: 'sin-bugbounty',
|
| 55 |
+
botName: 'SIN-BugBounty[bot]',
|
| 56 |
+
webhookPath: '/api/webhooks/github-apps',
|
| 57 |
+
webhookSecretEnv: 'SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET',
|
| 58 |
+
},
|
| 59 |
+
],
|
| 60 |
+
}),
|
| 61 |
+
SIN_HERMES_GITHUB_APP_ID: '123456',
|
| 62 |
+
SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET: 'secret-1',
|
| 63 |
+
SIN_BUGBOUNTY_GITHUB_APP_ID: '345678',
|
| 64 |
+
SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET: 'secret-3',
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
const { routeGitHubWebhook } = await importFresh('./github-app-routing.js');
|
| 69 |
+
const rawBody = JSON.stringify({
|
| 70 |
+
action: 'opened',
|
| 71 |
+
installation: { id: 987654321, app_id: 345678 },
|
| 72 |
+
repository: { full_name: 'OpenSIN-AI/OpenSIN-backend' },
|
| 73 |
+
});
|
| 74 |
+
const signature256 = `sha256=${createHmac('sha256', 'secret-3').update(rawBody).digest('hex')}`;
|
| 75 |
+
const result = await routeGitHubWebhook({
|
| 76 |
+
rawBody,
|
| 77 |
+
routePath: '/api/webhooks/github-apps',
|
| 78 |
+
signature256,
|
| 79 |
+
eventName: 'pull_request',
|
| 80 |
+
deliveryId: 'delivery-central-test',
|
| 81 |
+
});
|
| 82 |
+
assert.equal(result.ok, true);
|
| 83 |
+
assert.equal(result.route.agentSlug, 'sin-bugbounty');
|
| 84 |
+
assert.equal(result.route.botName, 'SIN-BugBounty[bot]');
|
| 85 |
+
} finally {
|
| 86 |
+
restoreEnv();
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
test('commentIssueAsGitHubApp exchanges installation token and posts comment', async () => {
|
| 91 |
+
const { privateKey } = generateKeyPairSync('rsa', {
|
| 92 |
+
modulusLength: 2048,
|
| 93 |
+
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
| 94 |
+
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
let commentRequestBody = '';
|
| 98 |
+
const server = createServer((req, res) => {
|
| 99 |
+
if (req.method === 'POST' && req.url === '/app/installations/987654321/access_tokens') {
|
| 100 |
+
res.writeHead(200, { 'content-type': 'application/json' });
|
| 101 |
+
res.end(JSON.stringify({ token: 'installation-token-1', expires_at: '2030-01-01T00:00:00Z' }));
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
if (req.method === 'POST' && req.url === '/repos/OpenSIN-AI/OpenSIN-backend/issues/42/comments') {
|
| 105 |
+
const chunks = [];
|
| 106 |
+
req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
| 107 |
+
req.on('end', () => {
|
| 108 |
+
commentRequestBody = Buffer.concat(chunks).toString('utf8');
|
| 109 |
+
res.writeHead(200, { 'content-type': 'application/json' });
|
| 110 |
+
res.end(JSON.stringify({ id: 9001, html_url: 'https://github.com/OpenSIN-AI/OpenSIN-backend/issues/42#issuecomment-9001' }));
|
| 111 |
+
});
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
res.writeHead(404, { 'content-type': 'application/json' });
|
| 115 |
+
res.end(JSON.stringify({ error: 'not_found' }));
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
| 119 |
+
const address = server.address();
|
| 120 |
+
const port = typeof address === 'object' && address ? address.port : 0;
|
| 121 |
+
const restoreEnv = setEnv({
|
| 122 |
+
SIN_GITHUB_API_BASE_URL: `http://127.0.0.1:${port}`,
|
| 123 |
+
SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({
|
| 124 |
+
defaultMode: 'hybrid',
|
| 125 |
+
defaultWebhookPath: '/github/webhook',
|
| 126 |
+
apps: [
|
| 127 |
+
{
|
| 128 |
+
agentSlug: 'sin-hermesbote',
|
| 129 |
+
appSlug: 'sin-hermes',
|
| 130 |
+
botName: 'SIN-Hermes[bot]',
|
| 131 |
+
appId: 123456,
|
| 132 |
+
privateKeyPem: privateKey,
|
| 133 |
+
webhookSecret: 'secret-1',
|
| 134 |
+
},
|
| 135 |
+
],
|
| 136 |
+
}),
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
try {
|
| 140 |
+
const { commentIssueAsGitHubApp } = await importFresh('./github-app-routing.js');
|
| 141 |
+
const result = await commentIssueAsGitHubApp({
|
| 142 |
+
repo: 'OpenSIN-AI/OpenSIN-backend',
|
| 143 |
+
issueNumber: 42,
|
| 144 |
+
body: 'Hello from SIN-Hermes[bot]',
|
| 145 |
+
agentSlug: 'sin-hermesbote',
|
| 146 |
+
installationId: 987654321,
|
| 147 |
+
});
|
| 148 |
+
assert.equal(result.ok, true);
|
| 149 |
+
assert.equal(result.agentSlug, 'sin-hermesbote');
|
| 150 |
+
assert.equal(result.commentId, 9001);
|
| 151 |
+
assert.match(commentRequestBody, /Hello from SIN-Hermes\[bot\]/);
|
| 152 |
+
} finally {
|
| 153 |
+
restoreEnv();
|
| 154 |
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
| 155 |
+
}
|
| 156 |
+
});
|