@sweny-ai/core 0.1.147 → 0.1.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-env.d.ts +9 -0
- package/dist/agent-env.js +61 -0
- package/dist/cli/config-file.d.ts +1 -1
- package/dist/cli/config-file.js +1 -1
- package/dist/cli/config.js +10 -3
- package/dist/cli/run-output.js +1 -1
- package/dist/harness/acp-rpc.d.ts +75 -0
- package/dist/harness/acp-rpc.js +204 -0
- package/dist/harness/acp.d.ts +189 -0
- package/dist/harness/acp.js +942 -0
- package/dist/harness/agents.d.ts +6 -2
- package/dist/harness/agents.js +10 -3
- package/dist/harness/capabilities.d.ts +58 -0
- package/dist/harness/capabilities.js +81 -0
- package/dist/harness/index.d.ts +11 -3
- package/dist/harness/index.js +11 -2
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.js +1 -1
- package/dist/harness/pi.d.ts +202 -0
- package/dist/harness/pi.js +966 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/agent-env.d.ts
CHANGED
|
@@ -57,6 +57,15 @@ export declare const CODEX_ENV_PREFIXES: readonly string[];
|
|
|
57
57
|
* Pure: returns a copy.
|
|
58
58
|
*/
|
|
59
59
|
export declare function resolveCodexAuthEnv(env: Record<string, string>): Record<string, string>;
|
|
60
|
+
/**
|
|
61
|
+
* pi's provider credentials (badlogic/pi-mono v0.99.2, docs/providers.md).
|
|
62
|
+
* sweny has no model opinion, so a pi run may need any provider's key; only
|
|
63
|
+
* these names (never the rest of the env) reach the pi process. A stored
|
|
64
|
+
* `auth.json` is not used: pi runs with a scratch `PI_CODING_AGENT_DIR`.
|
|
65
|
+
*/
|
|
66
|
+
export declare const PI_AUTH_VARS: readonly string[];
|
|
67
|
+
/** Prefixes for a pi run: locale only. `ANTHROPIC_*` / `CLAUDE_*` beyond the names above never reach pi. */
|
|
68
|
+
export declare const PI_ENV_PREFIXES: readonly string[];
|
|
60
69
|
/** A stored Codex login (`codex login`): `auth.json` under `CODEX_HOME`, default `~/.codex`. */
|
|
61
70
|
export declare function hasCodexLogin(env?: Record<string, string | undefined>, exists?: (p: string) => boolean): boolean;
|
|
62
71
|
export interface BuildAgentEnvOpts {
|
package/dist/agent-env.js
CHANGED
|
@@ -158,6 +158,67 @@ export function resolveCodexAuthEnv(env) {
|
|
|
158
158
|
out.CODEX_API_KEY = out.OPENAI_API_KEY;
|
|
159
159
|
return out;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* pi's provider credentials (badlogic/pi-mono v0.99.2, docs/providers.md).
|
|
163
|
+
* sweny has no model opinion, so a pi run may need any provider's key; only
|
|
164
|
+
* these names (never the rest of the env) reach the pi process. A stored
|
|
165
|
+
* `auth.json` is not used: pi runs with a scratch `PI_CODING_AGENT_DIR`.
|
|
166
|
+
*/
|
|
167
|
+
export const PI_AUTH_VARS = [
|
|
168
|
+
"ANTHROPIC_API_KEY",
|
|
169
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
170
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
171
|
+
"OPENAI_API_KEY",
|
|
172
|
+
"GEMINI_API_KEY",
|
|
173
|
+
"DEEPSEEK_API_KEY",
|
|
174
|
+
"MISTRAL_API_KEY",
|
|
175
|
+
"GROQ_API_KEY",
|
|
176
|
+
"CEREBRAS_API_KEY",
|
|
177
|
+
"XAI_API_KEY",
|
|
178
|
+
"OPENROUTER_API_KEY",
|
|
179
|
+
"AI_GATEWAY_API_KEY",
|
|
180
|
+
"ZAI_API_KEY",
|
|
181
|
+
"ZAI_CODING_CN_API_KEY",
|
|
182
|
+
"OPENCODE_API_KEY",
|
|
183
|
+
"RADIUS_API_KEY",
|
|
184
|
+
"TYPESAFE_API_KEY",
|
|
185
|
+
"HF_TOKEN",
|
|
186
|
+
"FIREWORKS_API_KEY",
|
|
187
|
+
"TOGETHER_API_KEY",
|
|
188
|
+
"BASETEN_API_KEY",
|
|
189
|
+
"KIMI_API_KEY",
|
|
190
|
+
"META_API_KEY",
|
|
191
|
+
"MINIMAX_API_KEY",
|
|
192
|
+
"MINIMAX_CN_API_KEY",
|
|
193
|
+
"MOONSHOT_API_KEY",
|
|
194
|
+
"NVIDIA_API_KEY",
|
|
195
|
+
"ANT_LING_API_KEY",
|
|
196
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
197
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
198
|
+
"XIAOMI_API_KEY",
|
|
199
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
200
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
201
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
202
|
+
"COPILOT_GITHUB_TOKEN",
|
|
203
|
+
"AZURE_OPENAI_API_KEY",
|
|
204
|
+
"AZURE_OPENAI_BASE_URL",
|
|
205
|
+
"AZURE_OPENAI_RESOURCE_NAME",
|
|
206
|
+
"CLOUDFLARE_API_KEY",
|
|
207
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
208
|
+
"CLOUDFLARE_GATEWAY_ID",
|
|
209
|
+
"GOOGLE_CLOUD_API_KEY",
|
|
210
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
211
|
+
"GCLOUD_PROJECT",
|
|
212
|
+
"GOOGLE_CLOUD_LOCATION",
|
|
213
|
+
"AWS_ACCESS_KEY_ID",
|
|
214
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
215
|
+
"AWS_SESSION_TOKEN",
|
|
216
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
217
|
+
"AWS_REGION",
|
|
218
|
+
"AWS_DEFAULT_REGION",
|
|
219
|
+
];
|
|
220
|
+
/** Prefixes for a pi run: locale only. `ANTHROPIC_*` / `CLAUDE_*` beyond the names above never reach pi. */
|
|
221
|
+
export const PI_ENV_PREFIXES = ["LC_"];
|
|
161
222
|
/** A stored Codex login (`codex login`): `auth.json` under `CODEX_HOME`, default `~/.codex`. */
|
|
162
223
|
export function hasCodexLogin(env = process.env, exists = existsSync) {
|
|
163
224
|
const home = env.CODEX_HOME || path.join(homedir(), ".codex");
|
|
@@ -19,4 +19,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
21
21
|
/** Starter config written by `sweny new`. */
|
|
22
|
-
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
|
22
|
+
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | pi\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -148,7 +148,7 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
148
148
|
# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file
|
|
149
149
|
# issue-tracker-provider: github-issues # github-issues | linear | jira
|
|
150
150
|
# source-control-provider: github # github | gitlab
|
|
151
|
-
# coding-agent-provider: claude # claude | codex
|
|
151
|
+
# coding-agent-provider: claude # claude | codex | pi
|
|
152
152
|
# notification-provider: console # console | slack | teams | discord | email | webhook
|
|
153
153
|
|
|
154
154
|
# ── Investigation ────────────────────────────────────────────────────
|
package/dist/cli/config.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { hasCodexLogin } from "../agent-env.js";
|
|
4
|
-
import { unsupportedAgentError } from "../harness/agents.js";
|
|
4
|
+
import { parseAcpAgent, unsupportedAgentError } from "../harness/agents.js";
|
|
5
5
|
export function registerTriageCommand(program) {
|
|
6
6
|
// NOTE: File-eligible options omit Commander defaults so parseCliInputs() can
|
|
7
7
|
// distinguish "user passed flag" from "not passed" and fall through to .sweny.yml.
|
|
8
8
|
return program
|
|
9
9
|
.command("triage")
|
|
10
10
|
.description("Run the SWEny triage workflow")
|
|
11
|
-
.option("--agent <provider>", "Coding agent: claude (default) or
|
|
11
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
12
12
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
13
13
|
.option("--observability-provider <provider>", "Observability provider (default: none)")
|
|
14
14
|
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
@@ -238,7 +238,14 @@ export function validateInputs(config) {
|
|
|
238
238
|
errors.push("Missing: CODEX_API_KEY or OPENAI_API_KEY, or a Codex login (`codex login`), for --agent codex");
|
|
239
239
|
}
|
|
240
240
|
break;
|
|
241
|
+
case "pi":
|
|
242
|
+
// pi takes any provider's key, or none (a local model in models.json), so
|
|
243
|
+
// there is no single credential to check here; pi reports its own auth error.
|
|
244
|
+
break;
|
|
241
245
|
default:
|
|
246
|
+
// An ACP agent (`acp:<command>`, #416) brings its own auth; it says so itself when it is missing.
|
|
247
|
+
if (parseAcpAgent(config.codingAgentProvider) !== undefined)
|
|
248
|
+
break;
|
|
242
249
|
// Honest --agent (#330): only agents with an adapter are accepted.
|
|
243
250
|
// Anything else would silently run a different agent under that name.
|
|
244
251
|
errors.push(unsupportedAgentError(config.codingAgentProvider));
|
|
@@ -658,7 +665,7 @@ export function registerImplementCommand(program) {
|
|
|
658
665
|
return program
|
|
659
666
|
.command("implement <issueId>")
|
|
660
667
|
.description("Implement a fix for a specific issue and open a PR")
|
|
661
|
-
.option("--agent <provider>", "Coding agent: claude (default) or
|
|
668
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
662
669
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
663
670
|
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
664
671
|
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
package/dist/cli/run-output.js
CHANGED
|
@@ -215,7 +215,7 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
215
215
|
"Write a PR-comment markdown (run receipt, status-colored DAG, per-node table; metadata only) to <path> so any CI can post it",
|
|
216
216
|
],
|
|
217
217
|
["--input <json>", "JSON string of input data to pass to the workflow"],
|
|
218
|
-
["--agent <id>", "Coding agent that runs the nodes: claude (default) or
|
|
218
|
+
["--agent <id>", "Coding agent that runs the nodes: claude (default), codex, or pi (experimental)"],
|
|
219
219
|
[
|
|
220
220
|
"--harness-policy <mode>",
|
|
221
221
|
"strict: refuse a node whose policy the agent cannot enforce; warn: run it and report what was not enforced (default: strict under GitHub Actions, warn elsewhere; env SWENY_HARNESS_POLICY)",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC 2.0 over a child process's stdio: the ACP stdio
|
|
3
|
+
* transport (agentclientprotocol.com/protocol/v1/transports, schema v1.24.1).
|
|
4
|
+
*
|
|
5
|
+
* - The client launches the agent as a subprocess and writes one JSON-RPC
|
|
6
|
+
* message per line to its stdin; the agent answers on stdout.
|
|
7
|
+
* - Messages MUST NOT contain embedded newlines (`JSON.stringify` never emits
|
|
8
|
+
* one).
|
|
9
|
+
* - The agent MAY log on stderr. It MUST NOT write anything but ACP messages
|
|
10
|
+
* to stdout; real agents sometimes do, so a stdout line that is not JSON is
|
|
11
|
+
* logged and skipped instead of ending the run.
|
|
12
|
+
*
|
|
13
|
+
* Hand-rolled on purpose: the wire is four message shapes, so sweny takes no
|
|
14
|
+
* SDK dependency for it, and the contract fake speaks the raw wire, which means
|
|
15
|
+
* the suite checks the protocol itself and not an SDK's reading of it.
|
|
16
|
+
*
|
|
17
|
+
* No agent SDK imports.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChildProcess } from "node:child_process";
|
|
20
|
+
/** JSON-RPC / ACP error codes (schema `ErrorCode`). */
|
|
21
|
+
export declare const RPC_ERROR: {
|
|
22
|
+
readonly parse: -32700;
|
|
23
|
+
readonly invalidRequest: -32600;
|
|
24
|
+
readonly methodNotFound: -32601;
|
|
25
|
+
readonly invalidParams: -32602;
|
|
26
|
+
readonly internal: -32603;
|
|
27
|
+
readonly requestCancelled: -32800;
|
|
28
|
+
readonly authRequired: -32000;
|
|
29
|
+
readonly resourceNotFound: -32002;
|
|
30
|
+
};
|
|
31
|
+
/** An error response from the peer (or a local failure reaching it). */
|
|
32
|
+
export declare class RpcError extends Error {
|
|
33
|
+
readonly code: number;
|
|
34
|
+
readonly data?: unknown | undefined;
|
|
35
|
+
constructor(message: string, code: number, data?: unknown | undefined);
|
|
36
|
+
}
|
|
37
|
+
export interface RpcHandlers {
|
|
38
|
+
/** A request from the peer. Resolve with the `result`, or throw {@link RpcError} for an error response. */
|
|
39
|
+
onRequest(method: string, params: unknown): Promise<unknown>;
|
|
40
|
+
/** A notification from the peer. */
|
|
41
|
+
onNotification(method: string, params: unknown): void;
|
|
42
|
+
/** A stdout line that was not a JSON-RPC message (debug only). */
|
|
43
|
+
onJunk?(line: string): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One JSON-RPC connection. Requests from us are matched to responses by id;
|
|
47
|
+
* requests from the peer are answered through {@link RpcHandlers.onRequest}.
|
|
48
|
+
* When the peer goes away every pending request rejects.
|
|
49
|
+
*/
|
|
50
|
+
export declare class RpcPeer {
|
|
51
|
+
private readonly child;
|
|
52
|
+
private readonly handlers;
|
|
53
|
+
private nextId;
|
|
54
|
+
private pending;
|
|
55
|
+
private buf;
|
|
56
|
+
private ended;
|
|
57
|
+
constructor(child: Pick<ChildProcess, "stdin" | "stdout">, handlers: RpcHandlers);
|
|
58
|
+
private onData;
|
|
59
|
+
private onLine;
|
|
60
|
+
private answer;
|
|
61
|
+
private write;
|
|
62
|
+
/** Send a request and wait for its response. */
|
|
63
|
+
request<T = unknown>(method: string, params: unknown): Promise<T>;
|
|
64
|
+
/** Send a notification (no response). */
|
|
65
|
+
notify(method: string, params: unknown): void;
|
|
66
|
+
/** The peer is gone: reject everything still waiting. Idempotent. */
|
|
67
|
+
end(reason: string): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Split a command line into argv: whitespace separates words, single and
|
|
71
|
+
* double quotes group, backslash escapes the next character outside single
|
|
72
|
+
* quotes. No expansion of any kind (no globbing, no variables, no shell), so
|
|
73
|
+
* the string after `acp:` can never run anything but the program it names.
|
|
74
|
+
*/
|
|
75
|
+
export declare function splitCommandLine(input: string): string[];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC 2.0 over a child process's stdio: the ACP stdio
|
|
3
|
+
* transport (agentclientprotocol.com/protocol/v1/transports, schema v1.24.1).
|
|
4
|
+
*
|
|
5
|
+
* - The client launches the agent as a subprocess and writes one JSON-RPC
|
|
6
|
+
* message per line to its stdin; the agent answers on stdout.
|
|
7
|
+
* - Messages MUST NOT contain embedded newlines (`JSON.stringify` never emits
|
|
8
|
+
* one).
|
|
9
|
+
* - The agent MAY log on stderr. It MUST NOT write anything but ACP messages
|
|
10
|
+
* to stdout; real agents sometimes do, so a stdout line that is not JSON is
|
|
11
|
+
* logged and skipped instead of ending the run.
|
|
12
|
+
*
|
|
13
|
+
* Hand-rolled on purpose: the wire is four message shapes, so sweny takes no
|
|
14
|
+
* SDK dependency for it, and the contract fake speaks the raw wire, which means
|
|
15
|
+
* the suite checks the protocol itself and not an SDK's reading of it.
|
|
16
|
+
*
|
|
17
|
+
* No agent SDK imports.
|
|
18
|
+
*/
|
|
19
|
+
/** JSON-RPC / ACP error codes (schema `ErrorCode`). */
|
|
20
|
+
export const RPC_ERROR = {
|
|
21
|
+
parse: -32700,
|
|
22
|
+
invalidRequest: -32600,
|
|
23
|
+
methodNotFound: -32601,
|
|
24
|
+
invalidParams: -32602,
|
|
25
|
+
internal: -32603,
|
|
26
|
+
requestCancelled: -32800,
|
|
27
|
+
authRequired: -32000,
|
|
28
|
+
resourceNotFound: -32002,
|
|
29
|
+
};
|
|
30
|
+
/** An error response from the peer (or a local failure reaching it). */
|
|
31
|
+
export class RpcError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
data;
|
|
34
|
+
constructor(message, code, data) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.data = data;
|
|
38
|
+
this.name = "RpcError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One JSON-RPC connection. Requests from us are matched to responses by id;
|
|
43
|
+
* requests from the peer are answered through {@link RpcHandlers.onRequest}.
|
|
44
|
+
* When the peer goes away every pending request rejects.
|
|
45
|
+
*/
|
|
46
|
+
export class RpcPeer {
|
|
47
|
+
child;
|
|
48
|
+
handlers;
|
|
49
|
+
nextId = 1;
|
|
50
|
+
pending = new Map();
|
|
51
|
+
buf = "";
|
|
52
|
+
ended;
|
|
53
|
+
constructor(child, handlers) {
|
|
54
|
+
this.child = child;
|
|
55
|
+
this.handlers = handlers;
|
|
56
|
+
child.stdout?.setEncoding("utf8");
|
|
57
|
+
child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
58
|
+
child.stdin?.on("error", () => {
|
|
59
|
+
// The agent exited before reading; the exit handling reports why.
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
onData(chunk) {
|
|
63
|
+
this.buf += chunk;
|
|
64
|
+
let nl;
|
|
65
|
+
while ((nl = this.buf.indexOf("\n")) !== -1) {
|
|
66
|
+
const line = this.buf.slice(0, nl);
|
|
67
|
+
this.buf = this.buf.slice(nl + 1);
|
|
68
|
+
this.onLine(line);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
onLine(raw) {
|
|
72
|
+
const line = raw.trim();
|
|
73
|
+
if (!line)
|
|
74
|
+
return;
|
|
75
|
+
let msg;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(line);
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
79
|
+
throw new Error("not an object");
|
|
80
|
+
msg = parsed;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
this.handlers.onJunk?.(line.length > 200 ? line.slice(0, 199) + "..." : line);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const id = msg.id;
|
|
87
|
+
const hasId = typeof id === "number" || typeof id === "string";
|
|
88
|
+
if (typeof msg.method === "string") {
|
|
89
|
+
if (hasId)
|
|
90
|
+
void this.answer(id, msg.method, msg.params);
|
|
91
|
+
else
|
|
92
|
+
this.handlers.onNotification(msg.method, msg.params);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!hasId)
|
|
96
|
+
return;
|
|
97
|
+
const p = this.pending.get(id);
|
|
98
|
+
if (!p)
|
|
99
|
+
return;
|
|
100
|
+
this.pending.delete(id);
|
|
101
|
+
const err = msg.error;
|
|
102
|
+
if (err && typeof err === "object") {
|
|
103
|
+
p.reject(new RpcError(typeof err.message === "string" ? err.message : `${p.method} failed`, typeof err.code === "number" ? err.code : RPC_ERROR.internal, err.data));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
p.resolve(msg.result);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async answer(id, method, params) {
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.handlers.onRequest(method, params);
|
|
112
|
+
this.write({ jsonrpc: "2.0", id, result: result ?? null });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const e = err instanceof RpcError
|
|
116
|
+
? err
|
|
117
|
+
: new RpcError(err instanceof Error ? err.message : String(err), RPC_ERROR.internal);
|
|
118
|
+
this.write({ jsonrpc: "2.0", id, error: { code: e.code, message: e.message } });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
write(msg) {
|
|
122
|
+
const stdin = this.child.stdin;
|
|
123
|
+
if (!stdin || stdin.destroyed || !stdin.writable)
|
|
124
|
+
return;
|
|
125
|
+
stdin.write(JSON.stringify(msg) + "\n");
|
|
126
|
+
}
|
|
127
|
+
/** Send a request and wait for its response. */
|
|
128
|
+
request(method, params) {
|
|
129
|
+
if (this.ended !== undefined)
|
|
130
|
+
return Promise.reject(new RpcError(this.ended, RPC_ERROR.internal));
|
|
131
|
+
const id = this.nextId++;
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
this.pending.set(id, { resolve: (v) => resolve(v), reject, method });
|
|
134
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Send a notification (no response). */
|
|
138
|
+
notify(method, params) {
|
|
139
|
+
if (this.ended !== undefined)
|
|
140
|
+
return;
|
|
141
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
142
|
+
}
|
|
143
|
+
/** The peer is gone: reject everything still waiting. Idempotent. */
|
|
144
|
+
end(reason) {
|
|
145
|
+
if (this.ended !== undefined)
|
|
146
|
+
return;
|
|
147
|
+
this.ended = reason;
|
|
148
|
+
// A final line without a newline is complete only if it parses.
|
|
149
|
+
if (this.buf.trim())
|
|
150
|
+
this.onLine(this.buf);
|
|
151
|
+
this.buf = "";
|
|
152
|
+
const pending = [...this.pending.values()];
|
|
153
|
+
this.pending.clear();
|
|
154
|
+
for (const p of pending)
|
|
155
|
+
p.reject(new RpcError(reason, RPC_ERROR.internal));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Split a command line into argv: whitespace separates words, single and
|
|
160
|
+
* double quotes group, backslash escapes the next character outside single
|
|
161
|
+
* quotes. No expansion of any kind (no globbing, no variables, no shell), so
|
|
162
|
+
* the string after `acp:` can never run anything but the program it names.
|
|
163
|
+
*/
|
|
164
|
+
export function splitCommandLine(input) {
|
|
165
|
+
const out = [];
|
|
166
|
+
let cur = "";
|
|
167
|
+
let started = false;
|
|
168
|
+
let quote = null;
|
|
169
|
+
for (let i = 0; i < input.length; i++) {
|
|
170
|
+
const ch = input[i];
|
|
171
|
+
if (quote) {
|
|
172
|
+
if (ch === quote)
|
|
173
|
+
quote = null;
|
|
174
|
+
else if (ch === "\\" && quote === '"' && i + 1 < input.length)
|
|
175
|
+
cur += input[++i];
|
|
176
|
+
else
|
|
177
|
+
cur += ch;
|
|
178
|
+
}
|
|
179
|
+
else if (ch === '"' || ch === "'") {
|
|
180
|
+
quote = ch;
|
|
181
|
+
started = true;
|
|
182
|
+
}
|
|
183
|
+
else if (ch === "\\" && i + 1 < input.length) {
|
|
184
|
+
cur += input[++i];
|
|
185
|
+
started = true;
|
|
186
|
+
}
|
|
187
|
+
else if (/\s/.test(ch)) {
|
|
188
|
+
if (started) {
|
|
189
|
+
out.push(cur);
|
|
190
|
+
cur = "";
|
|
191
|
+
started = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
cur += ch;
|
|
196
|
+
started = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (quote)
|
|
200
|
+
throw new Error(`unterminated ${quote} quote in "${input}"`);
|
|
201
|
+
if (started)
|
|
202
|
+
out.push(cur);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP harness (#416): runs a node on any Agent Client Protocol agent
|
|
3
|
+
* (`--agent acp:<command>`, for example `acp:opencode acp`,
|
|
4
|
+
* `acp:hermes acp`, `acp:goose acp`, `acp:gemini --experimental-acp`).
|
|
5
|
+
*
|
|
6
|
+
* ACP is the long-tail adapter. It is JSON-RPC 2.0 over the agent's stdio and
|
|
7
|
+
* carries a prompt and a stream of updates; it does not carry sweny's
|
|
8
|
+
* opinions. Read from agentclientprotocol/agent-client-protocol at tag
|
|
9
|
+
* schema-v1.24.1 (`schema/v1/schema.json`, `docs/protocol/v1/*.mdx`):
|
|
10
|
+
* - transports: newline-delimited JSON over stdio (`transports.mdx`).
|
|
11
|
+
* - `initialize` negotiates `protocolVersion` (1) and capabilities
|
|
12
|
+
* (`initialization.mdx`); `InitializeRequest` / `InitializeResponse`.
|
|
13
|
+
* - `session/new {cwd, mcpServers}` returns `{sessionId}`; every agent MUST
|
|
14
|
+
* support stdio MCP servers, http and sse are optional
|
|
15
|
+
* (`session-setup.mdx`, `McpServerStdio`, `McpServerHttp`).
|
|
16
|
+
* - `session/prompt {sessionId, prompt: ContentBlock[]}` streams
|
|
17
|
+
* `session/update` notifications (`agent_message_chunk`, `tool_call`,
|
|
18
|
+
* `tool_call_update`, `usage_update`, ...) and ends with
|
|
19
|
+
* `{stopReason}`; `PromptResponse` has no other field (`prompt-turn.mdx`).
|
|
20
|
+
* - `session/cancel` (notification) ends a turn with stopReason `cancelled`
|
|
21
|
+
* (`prompt-turn.mdx`, Cancellation); pending permission requests get the
|
|
22
|
+
* `cancelled` outcome.
|
|
23
|
+
* - `session/request_permission {toolCall, options}` is how an agent MAY ask
|
|
24
|
+
* before a tool runs; the client picks an option or answers `cancelled`
|
|
25
|
+
* (`tool-calls.mdx`, `RequestPermissionResponse`).
|
|
26
|
+
* - `fs/read_text_file` and `fs/write_text_file` are client methods an agent
|
|
27
|
+
* may use only when the client advertised them (`file-system.mdx`).
|
|
28
|
+
*
|
|
29
|
+
* Every sweny opinion is enforced by sweny here, refused or reported as
|
|
30
|
+
* `degraded` by `policyGate` (see ACP_CAPABILITIES for the honest list):
|
|
31
|
+
* - Structured output: the schema rides in the prompt; sweny parses the last
|
|
32
|
+
* JSON object, checks it and asks once more before giving up.
|
|
33
|
+
* - Tool deny and read-only: best effort through `session/request_permission`
|
|
34
|
+
* (denied classes and every non-read tool under `readOnly` are rejected),
|
|
35
|
+
* plus a refusal of every `fs/write_text_file`. The protocol has no deny list
|
|
36
|
+
* and an agent that does not ask is not stopped, so `policyGate` still
|
|
37
|
+
* reports deny and read-only as unenforced; strict refuses the node unless
|
|
38
|
+
* the process wrapper provides the read-only mount.
|
|
39
|
+
* - Sandbox and egress: none in the protocol. The whole agent process runs
|
|
40
|
+
* inside the sandbox wrapper (sandbox-wrapper.ts); strict refuses without it.
|
|
41
|
+
* - Env: sweny spawns the agent with the scoped env (agent-env.ts).
|
|
42
|
+
* - Skill tools: passed in `session/new` as the stdio MCP server of the tool
|
|
43
|
+
* bridge (#414), so the executor still observes every call.
|
|
44
|
+
* - Turn limit: a watchdog counts tool calls and cancels past the budget.
|
|
45
|
+
* - Timeout: `session/cancel`, a grace period, then the process is killed.
|
|
46
|
+
*/
|
|
47
|
+
import type { Logger, McpServerConfig, ToolContext } from "../types.js";
|
|
48
|
+
import { type SandboxMode } from "../agent-env.js";
|
|
49
|
+
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
|
|
50
|
+
import { ACP_CAPABILITIES } from "./capabilities.js";
|
|
51
|
+
import { type HarnessPolicyMode } from "./policy.js";
|
|
52
|
+
import { type SandboxWrapper } from "./sandbox-wrapper.js";
|
|
53
|
+
export { ACP_CAPABILITIES };
|
|
54
|
+
/** The ACP major version sweny speaks (`InitializeRequest.protocolVersion`). */
|
|
55
|
+
export declare const ACP_PROTOCOL_VERSION = 1;
|
|
56
|
+
export interface AcpHarnessOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The agent to run: a command line (`"opencode acp"`, the part after `acp:`)
|
|
59
|
+
* or an explicit `{command, args}`. Never run through a shell.
|
|
60
|
+
*/
|
|
61
|
+
acpCommand?: string | {
|
|
62
|
+
command: string;
|
|
63
|
+
args: string[];
|
|
64
|
+
};
|
|
65
|
+
/** Tool-call budget the watchdog enforces when a node sets no `max_turns` (default: 20). */
|
|
66
|
+
maxTurns?: number;
|
|
67
|
+
/** Working directory (default: process.cwd()). */
|
|
68
|
+
cwd?: string;
|
|
69
|
+
logger?: Logger;
|
|
70
|
+
/** Default tool context for standalone usage (not via the executor). */
|
|
71
|
+
defaultContext?: ToolContext;
|
|
72
|
+
/** Catalog defaults, overridden by per-run skill servers and explicit mcpServers. */
|
|
73
|
+
defaultMcpServers?: Record<string, McpServerConfig>;
|
|
74
|
+
/** Explicit external MCP servers, overriding defaults and per-run skill servers. */
|
|
75
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
76
|
+
/** Extra env var names passed through (see agent-env.ts). Default: `SWENY_ENV_PASSTHROUGH`. */
|
|
77
|
+
envPassthrough?: string[];
|
|
78
|
+
/** The agent's own auth env var names, always passed through when scoping (none are assumed). */
|
|
79
|
+
authVars?: string[];
|
|
80
|
+
/** Scope the agent env to the allowlist. Default: `SWENY_ENV_SCOPE`, else on in CI. */
|
|
81
|
+
envScope?: boolean;
|
|
82
|
+
/** Process sandbox mode. Default: `SWENY_SANDBOX`, else `auto` in CI and `off` locally. */
|
|
83
|
+
sandbox?: SandboxMode;
|
|
84
|
+
/** The host's process sandbox wrapper (#360 step 2). `undefined` = detect srt once per process; `null` = none (test seam). */
|
|
85
|
+
sandboxWrapper?: SandboxWrapper | null;
|
|
86
|
+
/** `strict` refuses a node with unenforceable opinions. Default: {@link resolveHarnessPolicy}. */
|
|
87
|
+
policy?: HarnessPolicyMode;
|
|
88
|
+
/** Hosts the agent itself needs (its model API), allowed through the sandbox wrapper. */
|
|
89
|
+
egress?: string[];
|
|
90
|
+
/** Shim command override for the tool bridge (test seam). */
|
|
91
|
+
toolBridgeShim?: {
|
|
92
|
+
command: string;
|
|
93
|
+
args: string[];
|
|
94
|
+
};
|
|
95
|
+
/** Grace period between SIGTERM and SIGKILL, and for the agent to exit on its own (default: 2000 ms). */
|
|
96
|
+
killGraceMs?: number;
|
|
97
|
+
/** How long the agent gets to answer `session/cancel` before it is killed (default: 5000 ms). */
|
|
98
|
+
cancelGraceMs?: number;
|
|
99
|
+
/** How long `initialize` and `session/new` may take (default: 60000 ms). */
|
|
100
|
+
startupTimeoutMs?: number;
|
|
101
|
+
}
|
|
102
|
+
/** Portable tool classes an ACP tool call can be, from its `kind` (schema `ToolKind`) and name. */
|
|
103
|
+
export declare function classifyAcpTool(kind: string | null | undefined, name: string | null | undefined): {
|
|
104
|
+
classes: ToolClass[];
|
|
105
|
+
readOnlySafe: boolean;
|
|
106
|
+
};
|
|
107
|
+
/** Does this tool call name one of sweny's skill tools (they reach the agent as MCP tools of the tool bridge)? */
|
|
108
|
+
export declare function isSkillToolCall(names: readonly (string | null | undefined)[], skillTools: readonly string[]): boolean;
|
|
109
|
+
interface PermissionOption {
|
|
110
|
+
optionId: string;
|
|
111
|
+
kind: string;
|
|
112
|
+
}
|
|
113
|
+
/** The permission outcome for an option list: the first option of a wanted kind, else `cancelled`. */
|
|
114
|
+
export declare function pickPermissionOutcome(options: readonly PermissionOption[], allow: boolean): {
|
|
115
|
+
outcome: "selected";
|
|
116
|
+
optionId: string;
|
|
117
|
+
} | {
|
|
118
|
+
outcome: "cancelled";
|
|
119
|
+
};
|
|
120
|
+
/** A loosely typed JSON object, for reading what the agent sends. */
|
|
121
|
+
type Obj = Record<string, any>;
|
|
122
|
+
/** `McpServerConfig` map to the ACP `mcpServers` array (schema `McpServer`). Returns what the agent cannot load too. */
|
|
123
|
+
export declare function toAcpMcpServers(servers: Record<string, McpServerConfig>, agentHttp: boolean): {
|
|
124
|
+
list: Obj[];
|
|
125
|
+
unsupported: string[];
|
|
126
|
+
};
|
|
127
|
+
export declare class AcpHarness implements AgentHarness {
|
|
128
|
+
readonly id: HarnessId;
|
|
129
|
+
readonly capabilities: HarnessCapabilities;
|
|
130
|
+
/** Empty on purpose: judges use the agent's own default model. */
|
|
131
|
+
readonly defaultJudgeModel = "";
|
|
132
|
+
readonly logger: Logger;
|
|
133
|
+
private agent;
|
|
134
|
+
private maxTurns;
|
|
135
|
+
private cwd;
|
|
136
|
+
private defaultContext;
|
|
137
|
+
private mcpServers;
|
|
138
|
+
private defaultMcpServers;
|
|
139
|
+
private envPassthrough;
|
|
140
|
+
private authVars;
|
|
141
|
+
private envScope;
|
|
142
|
+
private sandboxMode;
|
|
143
|
+
private sandboxWrapper;
|
|
144
|
+
private policyMode;
|
|
145
|
+
private egress;
|
|
146
|
+
private toolBridgeShim;
|
|
147
|
+
private killGraceMs;
|
|
148
|
+
private cancelGraceMs;
|
|
149
|
+
private startupTimeoutMs;
|
|
150
|
+
private agentVersion;
|
|
151
|
+
constructor(opts?: AcpHarnessOptions);
|
|
152
|
+
info(): HarnessInfo;
|
|
153
|
+
/**
|
|
154
|
+
* Is the agent command there? Deliberately does not start it: it would run
|
|
155
|
+
* outside the sandbox wrapper. The version comes from `initialize` on the
|
|
156
|
+
* first run (`agentInfo.version`).
|
|
157
|
+
*/
|
|
158
|
+
preflight(): Promise<{
|
|
159
|
+
ok: true;
|
|
160
|
+
version: string;
|
|
161
|
+
} | {
|
|
162
|
+
ok: false;
|
|
163
|
+
reason: string;
|
|
164
|
+
}>;
|
|
165
|
+
/** Env for the agent process: scoped like Claude Code's, with only the auth vars the operator named. */
|
|
166
|
+
private buildEnv;
|
|
167
|
+
/** The node policy, with the harness policy mode and the watchdog budget applied. */
|
|
168
|
+
private compilePolicy;
|
|
169
|
+
/**
|
|
170
|
+
* Talk to one agent process from `initialize` to the last `session/prompt`
|
|
171
|
+
* response, then stop it. Never throws: every failure is on the result.
|
|
172
|
+
*/
|
|
173
|
+
private converse;
|
|
174
|
+
/**
|
|
175
|
+
* Run a node. The gate runs first: in strict mode a node whose opinions an
|
|
176
|
+
* ACP agent cannot honor is refused before the agent starts. The agent
|
|
177
|
+
* process runs inside the host's sandbox wrapper (#360 step 2) when the
|
|
178
|
+
* sandbox mode asks for one and the host has one.
|
|
179
|
+
*/
|
|
180
|
+
run(req: HarnessRunRequest): Promise<HarnessRunResult>;
|
|
181
|
+
/** Map one conversation onto a NodeResult with Claude Code's semantics. */
|
|
182
|
+
private toResult;
|
|
183
|
+
/**
|
|
184
|
+
* One completion: a fresh session with no MCP servers, every permission
|
|
185
|
+
* request rejected, and any tool call over budget. null when it failed
|
|
186
|
+
* (callers fail closed).
|
|
187
|
+
*/
|
|
188
|
+
complete(req: HarnessCompleteRequest): Promise<string | null>;
|
|
189
|
+
}
|