@sweny-ai/core 0.1.176 → 0.1.178
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 +4 -0
- package/dist/cli/comment-output.js +4 -5
- package/dist/cli/config-file.d.ts +37 -2
- package/dist/cli/config-file.js +68 -3
- package/dist/cli/final-output.d.ts +4 -1
- package/dist/cli/final-output.js +6 -8
- package/dist/cli/main.d.ts +2 -1
- package/dist/cli/main.js +18 -10
- package/dist/cli/resume.js +1 -0
- package/dist/cli/run-history.d.ts +3 -7
- package/dist/cli/run-history.js +5 -12
- package/dist/cli/run-output.d.ts +16 -10
- package/dist/cli/run-output.js +30 -16
- package/dist/cli/ticket.js +3 -2
- package/dist/cli/try.js +2 -3
- package/dist/decider.d.ts +176 -64
- package/dist/decider.js +475 -155
- package/dist/executor.d.ts +55 -7
- package/dist/executor.js +511 -99
- package/dist/harness/acp.js +2 -2
- package/dist/harness/claude-code.js +6 -4
- package/dist/harness/codex.js +2 -2
- package/dist/harness/pi.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +34 -5
- package/dist/journal.js +42 -13
- package/dist/safe-file.d.ts +74 -0
- package/dist/safe-file.js +211 -0
- package/dist/safe-outputs.d.ts +7 -1
- package/dist/safe-outputs.js +7 -1
- package/dist/schema.d.ts +189 -241
- package/dist/schema.js +56 -36
- package/dist/skills/git-push.d.ts +99 -10
- package/dist/skills/git-push.js +413 -52
- package/dist/skills/github.d.ts +19 -15
- package/dist/skills/github.js +91 -15
- package/dist/startup-env.d.ts +30 -0
- package/dist/startup-env.js +48 -0
- package/dist/types.d.ts +86 -4
- package/dist/types.js +22 -0
- package/dist/when.d.ts +8 -1
- package/dist/when.js +8 -1
- package/dist/workflow-builder.js +3 -1
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +37 -5
- package/dist/workflows/triage.yml +40 -6
- package/package.json +1 -1
package/dist/agent-env.d.ts
CHANGED
|
@@ -322,7 +322,16 @@ export interface AgentAccess {
|
|
|
322
322
|
* hooks; only a sandbox stops a deliberate agent from undoing it.
|
|
323
323
|
*/
|
|
324
324
|
noPush?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* The run is staged or a dry run: no skill or external MCP server reaches
|
|
327
|
+
* the node (an MCP server cannot be classified per tool, so it may write).
|
|
328
|
+
* Only sweny's own tool bridge, already filtered to read tools, remains,
|
|
329
|
+
* and Claude Code runs with `strictMcpConfig`. Same MCP rule as `readOnly`.
|
|
330
|
+
*/
|
|
331
|
+
noMcp?: boolean;
|
|
325
332
|
}
|
|
333
|
+
/** True when a run request may load no MCP server but sweny's own bridge (read-only or staged). */
|
|
334
|
+
export declare function mcpWithheld(readOnly: boolean, access: Pick<AgentAccess, "noMcp"> | undefined): boolean;
|
|
326
335
|
/** Hosts reachable by sandboxed commands in every node: source hosting + package registries. */
|
|
327
336
|
export declare const DEFAULT_SANDBOX_DOMAINS: readonly string[];
|
|
328
337
|
/** Provider hosts added when a node references the built-in skill. */
|
package/dist/agent-env.js
CHANGED
|
@@ -814,6 +814,10 @@ export function grantedAgentEnv(agentEnv, opts) {
|
|
|
814
814
|
return { grant: [], dropped: `agent_env withheld in a staged or dry run: ${names.join(", ")}` };
|
|
815
815
|
return { grant: names };
|
|
816
816
|
}
|
|
817
|
+
/** True when a run request may load no MCP server but sweny's own bridge (read-only or staged). */
|
|
818
|
+
export function mcpWithheld(readOnly, access) {
|
|
819
|
+
return readOnly || access?.noMcp === true;
|
|
820
|
+
}
|
|
817
821
|
/** Hosts reachable by sandboxed commands in every node: source hosting + package registries. */
|
|
818
822
|
export const DEFAULT_SANDBOX_DOMAINS = [
|
|
819
823
|
"github.com",
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
|
|
7
7
|
* tool inputs, node outputs, errors, or model prose.
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
10
|
-
import path from "node:path";
|
|
9
|
+
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
11
10
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
11
|
import { formatReceiptDuration, nodeStates } from "./run-output.js";
|
|
13
12
|
import { formatTicketText } from "./ticket.js";
|
|
@@ -77,10 +76,10 @@ export function formatCrashComment(workflow, summary, runId) {
|
|
|
77
76
|
/** Write the comment markdown to `file`. Never throws; returns true only when written. */
|
|
78
77
|
export function writeRunComment(file, workflow, results, summary, opts = {}) {
|
|
79
78
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// The file may sit in the agent-writable workspace: no link on the path is followed.
|
|
80
|
+
writeFileNoFollow(file, opts.crashed
|
|
82
81
|
? formatCrashComment(workflow, summary, opts.runId)
|
|
83
|
-
: formatRunComment(workflow, results, summary, opts));
|
|
82
|
+
: formatRunComment(workflow, results, summary, opts), { ...workspaceRoot(file), mode: 0o644, dirMode: 0o755 });
|
|
84
83
|
return true;
|
|
85
84
|
}
|
|
86
85
|
catch (err) {
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys a workspace `.env` may never set. The workspace is agent-writable, and
|
|
3
|
+
* these name runtime or platform state sweny trusts as the operator's: CI and
|
|
4
|
+
* runner identity and the files the runner hands sweny to write
|
|
5
|
+
* (GITHUB_STEP_SUMMARY, GITHUB_OUTPUT, ...), where binaries are found (PATH),
|
|
6
|
+
* how Node and git behave (NODE_*, GIT_*), where traffic goes and which CAs it
|
|
7
|
+
* trusts (proxies, SSL/CA vars), and the home and temp dirs. A `.env` value
|
|
8
|
+
* for one of them is skipped with a warning. GITHUB_TOKEN is a credential the
|
|
9
|
+
* `.env` template documents, not platform state, so it stays allowed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DOTENV_DENIED_KEYS: RegExp;
|
|
12
|
+
/** True when a workspace `.env` may not set `key` (see {@link DOTENV_DENIED_KEYS}). */
|
|
13
|
+
export declare function isDotenvDenied(key: string): boolean;
|
|
1
14
|
/**
|
|
2
15
|
* Auto-load a `.env` file from the given directory.
|
|
3
|
-
* Sets `process.env[KEY]` only if not already defined (real env vars win)
|
|
16
|
+
* Sets `process.env[KEY]` only if not already defined (real env vars win),
|
|
17
|
+
* and never a runtime or platform key ({@link DOTENV_DENIED_KEYS}).
|
|
4
18
|
*/
|
|
5
19
|
export declare function loadDotenv(cwd?: string): void;
|
|
6
20
|
/**
|
|
@@ -10,6 +24,27 @@ export declare function loadDotenv(cwd?: string): void;
|
|
|
10
24
|
* treats as on/off.
|
|
11
25
|
*/
|
|
12
26
|
export declare function applyAgentFileConfig(fileConfig: FileConfig, env?: NodeJS.ProcessEnv): void;
|
|
27
|
+
/**
|
|
28
|
+
* Operator config for the decision model (#357), one trust domain per
|
|
29
|
+
* credential. `.sweny.yml` is repo content a pull request can change, so:
|
|
30
|
+
*
|
|
31
|
+
* - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
|
|
32
|
+
* SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
|
|
33
|
+
* (env) admits loopback or private addresses.
|
|
34
|
+
* - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
|
|
35
|
+
* Ollama) and never gets a key.
|
|
36
|
+
*
|
|
37
|
+
* `trusted(key)` reads an operator env value; it must not see values a
|
|
38
|
+
* committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
|
|
39
|
+
* Undefined when no URL and model are configured.
|
|
40
|
+
*/
|
|
41
|
+
export declare function operatorDeciderConfig(fileConfig: FileConfig, env: NodeJS.ProcessEnv, noDecider: boolean, trusted?: (key: string) => string | undefined): {
|
|
42
|
+
url: string;
|
|
43
|
+
model: string;
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
allowPrivate?: boolean;
|
|
46
|
+
loopbackOnly?: boolean;
|
|
47
|
+
} | false | undefined;
|
|
13
48
|
/** Parsed config file — flat strings for scalar fields, arrays for list fields, objects for nested blocks. */
|
|
14
49
|
export type FileConfig = Record<string, string | string[] | Record<string, unknown>>;
|
|
15
50
|
/**
|
|
@@ -19,4 +54,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
|
|
|
19
54
|
*/
|
|
20
55
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
21
56
|
/** 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 | 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";
|
|
57
|
+
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#\n# Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/\n# Local Ollama: set it here (a URL in this file must be loopback and never gets a key):\n# decider:\n# url: http://localhost:11434\n# model: nimble\n# Remote server: set it in the CI or shell environment only, never in this file or .env:\n# SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
+
// Imported for its side effect too: the startup env snapshot is taken before loadDotenv runs.
|
|
5
|
+
import { markWorkspaceEnv } from "../startup-env.js";
|
|
6
|
+
/**
|
|
7
|
+
* Keys a workspace `.env` may never set. The workspace is agent-writable, and
|
|
8
|
+
* these name runtime or platform state sweny trusts as the operator's: CI and
|
|
9
|
+
* runner identity and the files the runner hands sweny to write
|
|
10
|
+
* (GITHUB_STEP_SUMMARY, GITHUB_OUTPUT, ...), where binaries are found (PATH),
|
|
11
|
+
* how Node and git behave (NODE_*, GIT_*), where traffic goes and which CAs it
|
|
12
|
+
* trusts (proxies, SSL/CA vars), and the home and temp dirs. A `.env` value
|
|
13
|
+
* for one of them is skipped with a warning. GITHUB_TOKEN is a credential the
|
|
14
|
+
* `.env` template documents, not platform state, so it stays allowed.
|
|
15
|
+
*/
|
|
16
|
+
export const DOTENV_DENIED_KEYS = /^(GITHUB_(?!TOKEN$)[A-Za-z0-9_]*|RUNNER_[A-Za-z0-9_]*|ACTIONS_[A-Za-z0-9_]*|CI|PATH|Path|NODE_[A-Za-z0-9_]*|GIT_[A-Za-z0-9_]*|(HTTPS?|ALL|NO|FTP)_PROXY|(https?|all|no|ftp)_proxy|SSL_CERT_FILE|SSL_CERT_DIR|CURL_CA_BUNDLE|REQUESTS_CA_BUNDLE|TMPDIR|TEMP|TMP|HOME|USERPROFILE)$/;
|
|
17
|
+
/** True when a workspace `.env` may not set `key` (see {@link DOTENV_DENIED_KEYS}). */
|
|
18
|
+
export function isDotenvDenied(key) {
|
|
19
|
+
return DOTENV_DENIED_KEYS.test(key);
|
|
20
|
+
}
|
|
4
21
|
/**
|
|
5
22
|
* Auto-load a `.env` file from the given directory.
|
|
6
|
-
* Sets `process.env[KEY]` only if not already defined (real env vars win)
|
|
23
|
+
* Sets `process.env[KEY]` only if not already defined (real env vars win),
|
|
24
|
+
* and never a runtime or platform key ({@link DOTENV_DENIED_KEYS}).
|
|
7
25
|
*/
|
|
8
26
|
export function loadDotenv(cwd = process.cwd()) {
|
|
9
27
|
const envPath = path.join(cwd, ".env");
|
|
@@ -27,9 +45,14 @@ export function loadDotenv(cwd = process.cwd()) {
|
|
|
27
45
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
28
46
|
value = value.slice(1, -1);
|
|
29
47
|
}
|
|
30
|
-
if (process.env[key]
|
|
31
|
-
|
|
48
|
+
if (process.env[key] !== undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (isDotenvDenied(key)) {
|
|
51
|
+
process.stderr.write(` warning: .env sets ${key}, a runtime/platform variable; ignored\n`);
|
|
52
|
+
continue;
|
|
32
53
|
}
|
|
54
|
+
process.env[key] = value;
|
|
55
|
+
markWorkspaceEnv(key);
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
/**
|
|
@@ -68,6 +91,40 @@ export function applyAgentFileConfig(fileConfig, env = process.env) {
|
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Operator config for the decision model (#357), one trust domain per
|
|
96
|
+
* credential. `.sweny.yml` is repo content a pull request can change, so:
|
|
97
|
+
*
|
|
98
|
+
* - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
|
|
99
|
+
* SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
|
|
100
|
+
* (env) admits loopback or private addresses.
|
|
101
|
+
* - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
|
|
102
|
+
* Ollama) and never gets a key.
|
|
103
|
+
*
|
|
104
|
+
* `trusted(key)` reads an operator env value; it must not see values a
|
|
105
|
+
* committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
|
|
106
|
+
* Undefined when no URL and model are configured.
|
|
107
|
+
*/
|
|
108
|
+
export function operatorDeciderConfig(fileConfig, env, noDecider, trusted = (key) => env[key]) {
|
|
109
|
+
if (noDecider)
|
|
110
|
+
return false;
|
|
111
|
+
const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
|
|
112
|
+
const model = str(trusted("SWENY_DECIDER_MODEL")) ?? str(env.SWENY_DECIDER_MODEL) ?? str(fileConfig["decider.model"]);
|
|
113
|
+
const envUrl = str(trusted("SWENY_DECIDER_URL"));
|
|
114
|
+
if (envUrl) {
|
|
115
|
+
if (!model)
|
|
116
|
+
return undefined;
|
|
117
|
+
const apiKey = str(trusted("SWENY_DECIDER_API_KEY"));
|
|
118
|
+
const allowPrivate = ["1", "true"].includes((str(trusted("SWENY_DECIDER_ALLOW_PRIVATE")) ?? "").toLowerCase());
|
|
119
|
+
return { url: envUrl, model, ...(apiKey ? { apiKey } : {}), ...(allowPrivate ? { allowPrivate } : {}) };
|
|
120
|
+
}
|
|
121
|
+
// Repo content: a URL a committed `.env` put in the environment (untrusted),
|
|
122
|
+
// else the `.sweny.yml` one. Loopback only, never a key.
|
|
123
|
+
const repoUrl = str(env.SWENY_DECIDER_URL) ?? str(fileConfig["decider.url"]);
|
|
124
|
+
if (!repoUrl || !model)
|
|
125
|
+
return undefined;
|
|
126
|
+
return { url: repoUrl, model, loopbackOnly: true };
|
|
127
|
+
}
|
|
71
128
|
/**
|
|
72
129
|
* Search upward from `cwd` for `.sweny.yml` and parse it.
|
|
73
130
|
* Scalar values are strings, list values (rules, context) are string arrays.
|
|
@@ -273,4 +330,12 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
273
330
|
# Slack (notifications) — https://api.slack.com/apps
|
|
274
331
|
# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...
|
|
275
332
|
# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...
|
|
333
|
+
#
|
|
334
|
+
# Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/
|
|
335
|
+
# Local Ollama: set it here (a URL in this file must be loopback and never gets a key):
|
|
336
|
+
# decider:
|
|
337
|
+
# url: http://localhost:11434
|
|
338
|
+
# model: nimble
|
|
339
|
+
# Remote server: set it in the CI or shell environment only, never in this file or .env:
|
|
340
|
+
# SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...
|
|
276
341
|
`;
|
|
@@ -57,5 +57,8 @@ export declare function formatFinalOutput(out: FinalOutput, opts?: {
|
|
|
57
57
|
export declare function formatFinalMarkdown(workflow: Workflow, out: FinalOutput): string;
|
|
58
58
|
/** Relative path shown to the user: `.sweny/runs/<id>/output.md`. */
|
|
59
59
|
export declare function outputRelPath(runId: string): string;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Write output.md (mode 0600). Local only. Returns the relative path, or null on failure. Never throws.
|
|
62
|
+
* The run dir is agent-writable: no link on the path is followed (see safe-file.ts).
|
|
63
|
+
*/
|
|
61
64
|
export declare function writeFinalOutput(runId: string, markdown: string, cwd?: string): string | null;
|
package/dist/cli/final-output.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* (`--show-output`), since CI logs are often visible to more people than the
|
|
13
13
|
* repo's secrets are.
|
|
14
14
|
*/
|
|
15
|
-
import
|
|
15
|
+
import { writeFileNoFollow } from "../safe-file.js";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { collectSecretValues, redact } from "../journal.js";
|
|
18
18
|
import { RUN_HISTORY_DIR } from "./run-history.js";
|
|
@@ -186,16 +186,14 @@ export function formatFinalMarkdown(workflow, out) {
|
|
|
186
186
|
export function outputRelPath(runId) {
|
|
187
187
|
return path.posix.join(...RUN_HISTORY_DIR.split(path.sep), runId, "output.md");
|
|
188
188
|
}
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Write output.md (mode 0600). Local only. Returns the relative path, or null on failure. Never throws.
|
|
191
|
+
* The run dir is agent-writable: no link on the path is followed (see safe-file.ts).
|
|
192
|
+
*/
|
|
190
193
|
export function writeFinalOutput(runId, markdown, cwd = process.cwd()) {
|
|
191
194
|
try {
|
|
192
195
|
const rel = outputRelPath(runId);
|
|
193
|
-
|
|
194
|
-
fs.mkdirSync(path.dirname(path.dirname(file)), { recursive: true });
|
|
195
|
-
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
196
|
-
fs.writeFileSync(file, markdown, { mode: 0o600 });
|
|
197
|
-
// mode only applies on create: a resumed run overwrites an existing file.
|
|
198
|
-
fs.chmodSync(file, 0o600);
|
|
196
|
+
writeFileNoFollow(path.join(cwd, rel), markdown, { root: cwd, mode: 0o600 });
|
|
199
197
|
return rel;
|
|
200
198
|
}
|
|
201
199
|
catch {
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export declare function workflowRunAction(file: string | undefined, options: Rec
|
|
|
13
13
|
maxTokens?: string;
|
|
14
14
|
maxCost?: string;
|
|
15
15
|
yes?: boolean;
|
|
16
|
-
decider
|
|
16
|
+
/** False with `--no-decider`. */
|
|
17
|
+
decider?: boolean;
|
|
17
18
|
},
|
|
18
19
|
/** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
|
|
19
20
|
resume?: ResumeContext): Promise<void>;
|
package/dist/cli/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
const _require = createRequire(import.meta.url);
|
|
7
8
|
const { version } = _require("../../package.json");
|
|
@@ -28,7 +29,7 @@ import { toMermaidBlock } from "../mermaid.js";
|
|
|
28
29
|
import { runWorkflowDiagram } from "./diagram.js";
|
|
29
30
|
import { DagRenderer } from "./renderer.js";
|
|
30
31
|
import * as readline from "node:readline";
|
|
31
|
-
import { loadDotenv, loadConfigFile, applyAgentFileConfig } from "./config-file.js";
|
|
32
|
+
import { loadDotenv, loadConfigFile, applyAgentFileConfig, operatorDeciderConfig } from "./config-file.js";
|
|
32
33
|
import { buildCredentialMap } from "./credentials.js";
|
|
33
34
|
import { nonInteractiveUsage, runNew } from "./new.js";
|
|
34
35
|
import { formatFinalMarkdown, formatFinalOutput, formatSavedOutputLine, resolveFinalOutput, shouldPrintFinalOutput, writeFinalOutput, } from "./final-output.js";
|
|
@@ -57,6 +58,15 @@ import { beginCloudLifecycle, finishCloudLifecycle, createCloudStreamObserver }
|
|
|
57
58
|
import { runUpgrade, fetchLatestFromNpm } from "./upgrade.js";
|
|
58
59
|
import { maybeNudge, defaultCachePath } from "./version-check.js";
|
|
59
60
|
import { spawnSync } from "node:child_process";
|
|
61
|
+
import { trustedEnvValue } from "../startup-env.js";
|
|
62
|
+
/** GITHUB_STEP_SUMMARY as the operator set it: never a value a workspace `.env` introduced. */
|
|
63
|
+
function stepSummaryPath() {
|
|
64
|
+
return trustedEnvValue(process.env, "GITHUB_STEP_SUMMARY") || undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Append to GITHUB_STEP_SUMMARY without following a link (the path may sit in the workspace). */
|
|
67
|
+
function appendStepSummary(file, md) {
|
|
68
|
+
writeFileNoFollow(file, md, { ...workspaceRoot(file), append: true, mkdirs: false, mode: 0o644 });
|
|
69
|
+
}
|
|
60
70
|
/** Secret values a run can see, for redacting everything that prints node output. */
|
|
61
71
|
function outputSecrets(skills) {
|
|
62
72
|
return runSecretValues([...(skills?.values() ?? []), ...builtinSkills], process.env);
|
|
@@ -467,13 +477,13 @@ triageCmd.action(async (options) => {
|
|
|
467
477
|
console.log(formatDagResultHuman(results, durationMs, config));
|
|
468
478
|
}
|
|
469
479
|
// GitHub Actions step summary
|
|
470
|
-
if (config.notificationProvider === "github-summary" &&
|
|
480
|
+
if (config.notificationProvider === "github-summary" && stepSummaryPath()) {
|
|
471
481
|
try {
|
|
472
482
|
const md = formatDagResultMarkdown(results, durationMs, config, {
|
|
473
483
|
workflow: triageWorkflow,
|
|
474
484
|
trace,
|
|
475
485
|
});
|
|
476
|
-
|
|
486
|
+
appendStepSummary(stepSummaryPath(), md);
|
|
477
487
|
}
|
|
478
488
|
catch (err) {
|
|
479
489
|
// Don't fail the run if the summary file can't be written
|
|
@@ -531,9 +541,9 @@ triageCmd.action(async (options) => {
|
|
|
531
541
|
// silent — cloud reporting must never block or mask the crash
|
|
532
542
|
}
|
|
533
543
|
// Best-effort GitHub Actions step summary on crash
|
|
534
|
-
if (config.notificationProvider === "github-summary" &&
|
|
544
|
+
if (config.notificationProvider === "github-summary" && stepSummaryPath()) {
|
|
535
545
|
try {
|
|
536
|
-
|
|
546
|
+
appendStepSummary(stepSummaryPath(), `## ❌ SWEny Triage Crashed\n\n\`\`\`\n${crashMsg}\n\`\`\`\n`);
|
|
537
547
|
}
|
|
538
548
|
catch {
|
|
539
549
|
// ignore
|
|
@@ -743,10 +753,6 @@ export function loadWorkflowFile(filePath, knownSkills) {
|
|
|
743
753
|
export async function workflowRunAction(file, options,
|
|
744
754
|
/** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
|
|
745
755
|
resume) {
|
|
746
|
-
if (options.decider !== undefined && options.decider !== "off" && options.decider !== "shadow") {
|
|
747
|
-
console.error(chalk.red(`\n --decider must be off or shadow, got "${options.decider}"\n`));
|
|
748
|
-
process.exit(1);
|
|
749
|
-
}
|
|
750
756
|
// Reject junk --timeout/--max-steps up front (both paths) instead of
|
|
751
757
|
// silently falling back to a default.
|
|
752
758
|
let budget;
|
|
@@ -1043,7 +1049,9 @@ resume) {
|
|
|
1043
1049
|
fileRoot: config.fileRoot || undefined,
|
|
1044
1050
|
signal,
|
|
1045
1051
|
max_steps: wfMaxSteps,
|
|
1046
|
-
|
|
1052
|
+
// One trust domain per credential (#357): a URL, key or allowance a
|
|
1053
|
+
// committed .env introduced is repo content, read as untrusted.
|
|
1054
|
+
decider: operatorDeciderConfig(fileConfig, process.env, options.decider === false, (k) => trustedEnvValue(process.env, k)),
|
|
1047
1055
|
stageOutputs: options.stage === true,
|
|
1048
1056
|
journal,
|
|
1049
1057
|
...(spendBudget ? { budget: spendBudget } : {}),
|
package/dist/cli/resume.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* builds on this format, so readers skip records with a version they do not
|
|
11
11
|
* know instead of guessing.
|
|
12
12
|
*/
|
|
13
|
-
import type { ExecutionEvent, ExecutionTrace, NodeResult, Workflow } from "../types.js";
|
|
13
|
+
import type { ExecutionEvent, ExecutionTrace, NodeResult, RouteRung, Workflow } from "../types.js";
|
|
14
14
|
export declare const RUN_HISTORY_SCHEMA_VERSION = 1;
|
|
15
15
|
export declare const RUN_HISTORY_DIR: string;
|
|
16
16
|
export declare const RUN_HISTORY_KEEP = 200;
|
|
@@ -43,9 +43,11 @@ export interface RunRecord {
|
|
|
43
43
|
duration_ms: number;
|
|
44
44
|
status: RunStatus;
|
|
45
45
|
nodes: RunNodeRecord[];
|
|
46
|
+
/** Edges taken. `rung` says who chose a conditional route: expr, decider or agent (#357). */
|
|
46
47
|
routes: Array<{
|
|
47
48
|
from: string;
|
|
48
49
|
to: string;
|
|
50
|
+
rung?: RouteRung;
|
|
49
51
|
}>;
|
|
50
52
|
totals: RunTotals;
|
|
51
53
|
/** Harness that ran the nodes (id + version). Absent for runs with no harness-tagged result. */
|
|
@@ -55,12 +57,6 @@ export interface RunRecord {
|
|
|
55
57
|
};
|
|
56
58
|
/** Opinions the harness could not honor natively on some node, deduped. Absent when none. */
|
|
57
59
|
degraded?: string[];
|
|
58
|
-
/** Shadow-mode decider agreement counts (#357). Absent when the decider was off. */
|
|
59
|
-
decider?: {
|
|
60
|
-
compared: number;
|
|
61
|
-
agreed: number;
|
|
62
|
-
fell_through: number;
|
|
63
|
-
};
|
|
64
60
|
}
|
|
65
61
|
/** sha256 (hex) of the workflow as canonical JSON: keys sorted, undefined dropped. */
|
|
66
62
|
export declare function hashWorkflow(workflow: Workflow): string;
|
package/dist/cli/run-history.js
CHANGED
|
@@ -14,6 +14,7 @@ import crypto from "node:crypto";
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { summarizeRun } from "./run-output.js";
|
|
17
|
+
import { openNoFollow } from "../safe-file.js";
|
|
17
18
|
export const RUN_HISTORY_SCHEMA_VERSION = 1;
|
|
18
19
|
export const RUN_HISTORY_DIR = path.join(".sweny", "runs");
|
|
19
20
|
export const RUN_HISTORY_KEEP = 200;
|
|
@@ -102,7 +103,7 @@ export function buildRunRecord(i) {
|
|
|
102
103
|
duration_ms: Math.max(0, Math.round(i.durationMs)),
|
|
103
104
|
status: i.crashed ? "crashed" : summary.ok ? "success" : "failed",
|
|
104
105
|
nodes,
|
|
105
|
-
routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to })),
|
|
106
|
+
routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to, ...(e.rung ? { rung: e.rung } : {}) })),
|
|
106
107
|
totals: {
|
|
107
108
|
nodes_total: summary.nodesTotal,
|
|
108
109
|
nodes_ok: summary.nodesOk,
|
|
@@ -113,15 +114,6 @@ export function buildRunRecord(i) {
|
|
|
113
114
|
},
|
|
114
115
|
...(harness ? { harness: { id: harness.id, version: harness.version } } : {}),
|
|
115
116
|
...(degraded.length > 0 ? { degraded } : {}),
|
|
116
|
-
...(summary.decider
|
|
117
|
-
? {
|
|
118
|
-
decider: {
|
|
119
|
-
compared: summary.decider.compared,
|
|
120
|
-
agreed: summary.decider.agreed,
|
|
121
|
-
fell_through: summary.decider.fellThrough,
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
: {}),
|
|
125
117
|
};
|
|
126
118
|
}
|
|
127
119
|
// ── Write, prune, read ──────────────────────────────────────────
|
|
@@ -131,8 +123,9 @@ export function writeRunRecord(record, cwd = process.cwd()) {
|
|
|
131
123
|
const final = path.join(dir, `${record.run_id}.json`);
|
|
132
124
|
const tmp = path.join(dir, `.${record.run_id}.json.${process.pid}.tmp`);
|
|
133
125
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
// The workspace is agent-writable: no link on the path is followed (safe-file.ts).
|
|
127
|
+
// rename() replaces a link at `final` itself, never its target.
|
|
128
|
+
const fd = openNoFollow(tmp, { root: cwd, mode: 0o644 });
|
|
136
129
|
try {
|
|
137
130
|
fs.writeSync(fd, JSON.stringify(record, null, 2) + "\n");
|
|
138
131
|
fs.fsyncSync(fd);
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -40,15 +40,19 @@ export interface RunSummary {
|
|
|
40
40
|
spent: number;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* Absent when
|
|
43
|
+
* Who decided the run's conditional routes (#357), counts only: a `when`
|
|
44
|
+
* expression, the decision model, or the agent. The decider and expression
|
|
45
|
+
* counts are routes with no agent call. Absent when no route was a decision.
|
|
46
46
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
routes?: RouteCounts;
|
|
48
|
+
/** Why the decider did not run for some or all routes (missing config, breaker, cap). */
|
|
49
|
+
deciderOff?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface RouteCounts {
|
|
52
|
+
total: number;
|
|
53
|
+
decider: number;
|
|
54
|
+
expr: number;
|
|
55
|
+
agent: number;
|
|
52
56
|
}
|
|
53
57
|
export interface RunPolicy {
|
|
54
58
|
/** Agent env narrowed to the allowlist. Absent when no node reported it. */
|
|
@@ -68,6 +72,8 @@ export interface RunPolicy {
|
|
|
68
72
|
export declare function degradedKey(entry: string): string;
|
|
69
73
|
/** Sum the per-node results into one run summary. */
|
|
70
74
|
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean, trace?: ExecutionTrace): RunSummary;
|
|
75
|
+
/** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
|
|
76
|
+
export declare function countRoutes(trace: ExecutionTrace): RouteCounts;
|
|
71
77
|
/**
|
|
72
78
|
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
73
79
|
* `1 output staged`. No phrase for a fact no node reported.
|
|
@@ -85,8 +91,8 @@ export declare function formatCost(usd: number): string;
|
|
|
85
91
|
export declare function formatBudgetAmount(b: NonNullable<RunSummary["budget"]>): string;
|
|
86
92
|
/** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
|
|
87
93
|
export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>): string;
|
|
88
|
-
/** `
|
|
89
|
-
export declare function
|
|
94
|
+
/** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
|
|
95
|
+
export declare function formatRouteCounts(r: RouteCounts): string;
|
|
90
96
|
/**
|
|
91
97
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
92
98
|
* Token and cost segments are omitted when the SDK reported none. A harness
|
package/dist/cli/run-output.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* METADATA ONLY. Nothing here reads prompts, node outputs, tool inputs, or
|
|
6
6
|
* model prose: only counts, durations, token totals, and cost.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
8
|
+
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
9
9
|
import { consoleLogger } from "../types.js";
|
|
10
|
-
import { summarizeDecisions } from "../decider.js";
|
|
11
10
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
11
|
import { createPaint } from "./style.js";
|
|
13
12
|
import { colorEnabled, glyphs } from "./terminal.js";
|
|
13
|
+
import { trustedEnvValue } from "../startup-env.js";
|
|
14
14
|
/** The short key of a `degraded` entry: the text before its first colon. */
|
|
15
15
|
export function degradedKey(entry) {
|
|
16
16
|
const i = entry.indexOf(":");
|
|
@@ -83,12 +83,20 @@ export function summarizeRun(results, durationMs, crashed = false, trace) {
|
|
|
83
83
|
...(degraded.size > 0 ? { degraded: [...degraded] } : {}),
|
|
84
84
|
...(Object.keys(policy).length > 0 ? { policy } : {}),
|
|
85
85
|
...(budget ? { budget } : {}),
|
|
86
|
-
...(trace
|
|
86
|
+
...(trace && countRoutes(trace).total > 0 ? { routes: countRoutes(trace) } : {}),
|
|
87
|
+
...(trace?.deciderOff ? { deciderOff: trace.deciderOff } : {}),
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
/** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
|
|
91
|
+
export function countRoutes(trace) {
|
|
92
|
+
const c = { total: 0, decider: 0, expr: 0, agent: 0 };
|
|
93
|
+
for (const e of trace.edges) {
|
|
94
|
+
if (!e.rung)
|
|
95
|
+
continue;
|
|
96
|
+
c.total++;
|
|
97
|
+
c[e.rung]++;
|
|
98
|
+
}
|
|
99
|
+
return c;
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
102
|
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
@@ -156,9 +164,10 @@ export function formatBudgetAmount(b) {
|
|
|
156
164
|
export function formatBudgetOverrun(b) {
|
|
157
165
|
return `budget exceeded: ${formatBudgetAmount(b)} (${b.scope}, at node ${b.node})`;
|
|
158
166
|
}
|
|
159
|
-
/** `
|
|
160
|
-
export function
|
|
161
|
-
|
|
167
|
+
/** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
|
|
168
|
+
export function formatRouteCounts(r) {
|
|
169
|
+
const parts = ["decider", "expr", "agent"].filter((k) => r[k] > 0).map((k) => `${r[k]} ${k}`);
|
|
170
|
+
return parts.length > 0 ? `${r.total} (${parts.join(", ")})` : String(r.total);
|
|
162
171
|
}
|
|
163
172
|
/**
|
|
164
173
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
@@ -179,7 +188,8 @@ export function formatReceipt(s) {
|
|
|
179
188
|
...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
|
|
180
189
|
...(formatPolicySegment(s.policy) ? [formatPolicySegment(s.policy)] : []),
|
|
181
190
|
...(s.budget ? [formatBudgetOverrun(s.budget)] : []),
|
|
182
|
-
...(s.
|
|
191
|
+
...(s.routes && s.routes.total > 0 ? [`routes ${formatRouteCounts(s.routes)}`] : []),
|
|
192
|
+
...(s.deciderOff ? [`decider off: ${s.deciderOff}`] : []),
|
|
183
193
|
];
|
|
184
194
|
return parts.join(" · ");
|
|
185
195
|
}
|
|
@@ -219,11 +229,18 @@ export function formatStepSummary(workflow, results, summary, trace) {
|
|
|
219
229
|
* Never throws; returns true only when something was written.
|
|
220
230
|
*/
|
|
221
231
|
export function writeStepSummary(workflow, results, summary, trace, env = process.env) {
|
|
222
|
-
|
|
232
|
+
// The operator's value only: never one a workspace `.env` introduced.
|
|
233
|
+
const file = trustedEnvValue(env, "GITHUB_STEP_SUMMARY");
|
|
223
234
|
if (!file)
|
|
224
235
|
return false;
|
|
225
236
|
try {
|
|
226
|
-
|
|
237
|
+
// Never through a link, in case the path sits in the agent-writable workspace.
|
|
238
|
+
writeFileNoFollow(file, formatStepSummary(workflow, results, summary, trace), {
|
|
239
|
+
...workspaceRoot(file),
|
|
240
|
+
append: true,
|
|
241
|
+
mkdirs: false,
|
|
242
|
+
mode: 0o644,
|
|
243
|
+
});
|
|
227
244
|
return true;
|
|
228
245
|
}
|
|
229
246
|
catch (err) {
|
|
@@ -309,8 +326,5 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
309
326
|
"--harness-policy <mode>",
|
|
310
327
|
"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)",
|
|
311
328
|
],
|
|
312
|
-
[
|
|
313
|
-
"--decider <mode>",
|
|
314
|
-
"Decision model for route choices: off (default) or shadow. Shadow asks the workflow's decider.provider alongside the agent and reports agreement; the route is always the agent's. Needs decider.provider in the workflow (no default URL)",
|
|
315
|
-
],
|
|
329
|
+
["--no-decider", "Skip the decision model for this run: the agent decides the routes of route_by: decider nodes"],
|
|
316
330
|
];
|