quorumkit-engine 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASING.md +151 -0
- package/SECURITY.md +82 -0
- package/action.yml +25 -0
- package/dist/212.index.js +171 -0
- package/dist/212.index.js.map +1 -0
- package/dist/563.index.js +100 -0
- package/dist/563.index.js.map +1 -0
- package/dist/992.index.js +211 -0
- package/dist/992.index.js.map +1 -0
- package/dist/agent-identities.schema.json +33 -0
- package/dist/apm-msg.schema.json +62 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/licenses.txt +1 -0
- package/dist/package.json +3 -0
- package/dist/pipeline.schema.json +108 -0
- package/dist/runtimes.schema.json +43 -0
- package/dist/sourcemap-register.cjs +1 -0
- package/package.json +21 -0
package/RELEASING.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# QuorumKit Engine — Release Procedure
|
|
2
|
+
|
|
3
|
+
> **Audience:** maintainers cutting a release of `engine/`. Contributors should not need to read this.
|
|
4
|
+
|
|
5
|
+
This document is the operational counterpart to `engine/SECURITY.md` and
|
|
6
|
+
the authoritative procedure referenced by FR-010, FR-028, SEC-MED-004,
|
|
7
|
+
and SC-009 in `specs/047-repo-topology/spec.md`.
|
|
8
|
+
|
|
9
|
+
The engine ships through **two channels**, both released atomically from
|
|
10
|
+
the same `engine/` source on every signed `v*` tag:
|
|
11
|
+
|
|
12
|
+
1. **GitHub Action ref** — consumers pin via
|
|
13
|
+
`uses: dmitry-nalivaika/quorumkit/engine@vX.Y.Z` (or `@vX` for floating major).
|
|
14
|
+
2. **npm package `quorumkit-engine`** — published with `--provenance` via
|
|
15
|
+
OIDC trusted publishing (no `NPM_TOKEN`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Release prerequisites (one-time setup)
|
|
20
|
+
|
|
21
|
+
| Setup task | Where | Who |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Maintainer GPG key registered with GitHub & in the local keyring | `gpg --import` + `git config user.signingkey <FPR>` + `gpg --export <FPR>` uploaded to GitHub | Maintainer |
|
|
24
|
+
| `release` GitHub Environment with required-reviewer protection rule | Repo Settings → Environments → `release` | Maintainer (admin) |
|
|
25
|
+
| npm "Trusted Publisher" mapping for `quorumkit-engine` | <https://www.npmjs.com/package/quorumkit-engine/access> → Trusted Publishers → add this repo + workflow `engine-release.yml` | Maintainer + npm package owner |
|
|
26
|
+
| Branch-protection on `main` requires PR + verify-mirror green | Repo Settings → Branches → `main` | Maintainer (admin) |
|
|
27
|
+
|
|
28
|
+
If any of these are missing, the release workflow fails fast — you never
|
|
29
|
+
publish a half-configured artefact by accident.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Release procedure (per release)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 1. Make sure you are on a clean main with the new engine source committed.
|
|
37
|
+
git checkout main && git pull --ff-only
|
|
38
|
+
bash installer/verify-mirror.sh # M1–M9 must be green
|
|
39
|
+
(cd engine/orchestrator && npm test) # all engine tests pass
|
|
40
|
+
(cd engine && npm ci --ignore-scripts && npm run build)
|
|
41
|
+
git status # MUST be clean (dist/ in sync)
|
|
42
|
+
|
|
43
|
+
# 2. Bump quorumkit.yml (T-25) and CHANGELOG.md (T-24) in a release PR. Get review
|
|
44
|
+
# + Security Agent sign-off. Merge.
|
|
45
|
+
|
|
46
|
+
# 3. Cut a SIGNED tag at the merge commit on main.
|
|
47
|
+
git tag -s v3.0.0 -m "QuorumKit v3.0.0 — three-zone topology + engine distribution"
|
|
48
|
+
git push origin v3.0.0
|
|
49
|
+
|
|
50
|
+
# 4. The engine-release.yml workflow auto-runs:
|
|
51
|
+
# a. git verify-tag — refuses unsigned tags
|
|
52
|
+
# b. rebuild engine/dist/ via ncc — refuses tag → bundle drift
|
|
53
|
+
# c. npm publish --provenance — OIDC, no NPM_TOKEN
|
|
54
|
+
# d. github-script creates the GitHub Release with auto-notes
|
|
55
|
+
# The 'release' Environment requires a maintainer reviewer click before
|
|
56
|
+
# step (c) runs — that is the supply-chain gate.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A successful run produces:
|
|
60
|
+
|
|
61
|
+
- A GitHub Release at <https://github.com/dmitry-nalivaika/quorumkit/releases/tag/vX.Y.Z>.
|
|
62
|
+
- An npm package at <https://www.npmjs.com/package/quorumkit-engine/v/X.Y.Z>
|
|
63
|
+
whose page shows the **"Built and signed on GitHub Actions"** provenance
|
|
64
|
+
badge with a link to the workflow run that produced it.
|
|
65
|
+
- A signed git tag verifiable with `git verify-tag vX.Y.Z`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. Verifying a release as a downstream user
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Action ref (what consumer workflows pin):
|
|
73
|
+
gh api repos/dmitry-nalivaika/quorumkit/git/refs/tags/vX.Y.Z
|
|
74
|
+
|
|
75
|
+
# Tag signature:
|
|
76
|
+
git fetch --tags
|
|
77
|
+
git verify-tag vX.Y.Z
|
|
78
|
+
|
|
79
|
+
# npm provenance:
|
|
80
|
+
npm view quorumkit-engine@X.Y.Z --json | jq '.dist'
|
|
81
|
+
# Expect: dist.attestations.provenance is present and signed by the
|
|
82
|
+
# GitHub Actions OIDC issuer for this repository.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The maintainer's GPG public key fingerprint is published at
|
|
86
|
+
`docs/architecture/adr-047-action-runtime.md` §Verifying-key. Rotate the
|
|
87
|
+
fingerprint there — and only there — when the maintainer's signing key
|
|
88
|
+
changes.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 4. Floating major-version tag (`v3`)
|
|
93
|
+
|
|
94
|
+
After every patch / minor release on the `v3.x` series, fast-forward the
|
|
95
|
+
floating `v3` tag so consumers pinned to `@v3` automatically receive the
|
|
96
|
+
update:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git tag -fs v3 -m "Track v3.0.1" # signed, force-update
|
|
100
|
+
git push --force origin v3
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Floating tags are **only** acceptable for first-party QuorumKit Actions inside
|
|
104
|
+
`templates/github/workflows/`. Third-party Actions remain SHA-pinned per
|
|
105
|
+
FR-031 / mirror gate M9 — Dependabot rotates them.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 5. Rollback
|
|
110
|
+
|
|
111
|
+
If a release ships a regression:
|
|
112
|
+
|
|
113
|
+
1. **Yank the npm version** (does not delete; warns installers):
|
|
114
|
+
```bash
|
|
115
|
+
npm deprecate quorumkit-engine@X.Y.Z "Yanked — see #<issue>; upgrade to X.Y.Z+1"
|
|
116
|
+
```
|
|
117
|
+
2. **Re-publish the prior known-good** as the latest dist-tag:
|
|
118
|
+
```bash
|
|
119
|
+
npm dist-tag add quorumkit-engine@X.Y.(Z-1) latest
|
|
120
|
+
```
|
|
121
|
+
3. **Move the floating major tag back** so consumers pinned to `@v3`
|
|
122
|
+
pick up the rollback on their next workflow run:
|
|
123
|
+
```bash
|
|
124
|
+
git tag -fs v3 -m "Rollback to vX.Y.(Z-1)" <good-commit-sha>
|
|
125
|
+
git push --force origin v3
|
|
126
|
+
```
|
|
127
|
+
4. Open an incident issue with the `incident` label so the Incident Agent
|
|
128
|
+
timestamps the postmortem (per `.apm/agents/incident-agent.md`).
|
|
129
|
+
5. Cut a fixed `vX.Y.(Z+1)` per §2 once the regression is patched.
|
|
130
|
+
|
|
131
|
+
`npm unpublish` is **forbidden** — it breaks reproducibility for any
|
|
132
|
+
consumer who already pinned to the bad version. Always deprecate + supersede.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 6. Disaster recovery — fallback NPM token (SEC-HIGH-001)
|
|
137
|
+
|
|
138
|
+
If npm trusted publishing is unavailable (e.g. a registry-side outage or
|
|
139
|
+
the maintainer needs to publish from an air-gapped host), and only then,
|
|
140
|
+
a fallback token MAY be used under the following constraints:
|
|
141
|
+
|
|
142
|
+
- **Granular**, package-scoped (`quorumkit-engine` only), publish-only.
|
|
143
|
+
- **≤90-day expiry** — never rotated to a longer lifetime.
|
|
144
|
+
- Stored in the `release` GitHub Environment **only**; never in repo
|
|
145
|
+
secrets, never in `.env`, never in a maintainer's shell history.
|
|
146
|
+
- Rotation owner: the maintainer named in `quorumkit.yml` `owner.npm`.
|
|
147
|
+
- Each use of the fallback MUST be paired with a public Security Agent
|
|
148
|
+
comment on the corresponding incident issue.
|
|
149
|
+
|
|
150
|
+
Removing the fallback token after the trusted-publishing path is restored
|
|
151
|
+
is the responsibility of the same maintainer.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# QuorumKit Engine — Security & Permissions
|
|
2
|
+
|
|
3
|
+
> **Audience:** consumer-repo maintainers wiring `uses: dmitry-nalivaika/quorumkit/engine@<sha>` into a workflow, and QuorumKit contributors changing engine behaviour.
|
|
4
|
+
|
|
5
|
+
This document is the security contract of the QuorumKit engine Action. It is
|
|
6
|
+
authoritative for FR-014, ADR-047 amendment §4, and SEC-MED-001. Every
|
|
7
|
+
permission the engine consumes MUST appear in the table below with a
|
|
8
|
+
written justification; reviewers reject changes that broaden the table
|
|
9
|
+
without a paired spec amendment.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Default permission posture
|
|
14
|
+
|
|
15
|
+
The engine declares **zero** permissions in `engine/action.yml`. Permissions
|
|
16
|
+
are granted at the **call site** by each consumer workflow. The minimum
|
|
17
|
+
viable token for a feature pipeline is:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read # default — required for actions/checkout
|
|
22
|
+
issues: write # post audit comments, update labels
|
|
23
|
+
pull-requests: write # comment on PRs and update review state
|
|
24
|
+
actions: read # read workflow_run payloads
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The engine **MUST NOT** be invoked with `permissions: write-all`.
|
|
28
|
+
`installer/init.sh --upgrade` (T-20) refuses to broaden the consumer's
|
|
29
|
+
existing `permissions:` block.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Per-scope justification table
|
|
34
|
+
|
|
35
|
+
| Scope | Default access | Why the engine needs it | When to omit |
|
|
36
|
+
| ---------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
37
|
+
| `contents: read` | required | `actions/checkout` reads the consumer repo to load `.apm/pipelines/`, `.apm/agents/`, and other SoT files. | never — without this the engine cannot start. |
|
|
38
|
+
| `contents: write` | **off** | Only enable for consumer pipelines that include a step writing files via the orchestrator (e.g. dashboard regen). Most pipelines don't. | default — keep `contents: read`. |
|
|
39
|
+
| `issues: write` | required | Post audit comments (FR-026, ADR-002), `<QUORUMKIT-LIVE-STATUS>` updates, and label changes that drive the state machine. | only if the consumer does not use issue-driven pipelines. |
|
|
40
|
+
| `pull-requests: write` | required | Comment on PR threads, update review labels, and post timeline reconstructions. | only if the consumer disables PR-triggered pipelines. |
|
|
41
|
+
| `actions: read` | required | Read `workflow_run` payloads to recover issue context (ADR-007 §6) and to re-correlate audit comments after agent failures. | only if the pipeline never depends on `workflow_run` events. |
|
|
42
|
+
| `actions: write` | **off** | Never required by the engine itself. Forbid in consumer workflows unless a custom step explicitly needs `gh workflow run` capability. | always — no engine code path requests it. |
|
|
43
|
+
| `id-token: write` | release-only | Used **only** by the engine's own release workflow (`engine-release.yml`) for OIDC-trusted npm publishing. Never granted to consumer workflows. | always for consumers. |
|
|
44
|
+
| `packages: read/write` | **off** | The engine does not pull or push packages from `ghcr.io`. | always — keep off. |
|
|
45
|
+
| `deployments: write` | **off** | The engine never creates a Deployment resource. | always — keep off. |
|
|
46
|
+
| `security-events: write` | **off** | Code scanning / Dependabot is enforced by `dependabot.yml` and the GitHub-hosted scanners, not by the engine. | always — keep off. |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3. Threat model snapshot (SEC-MED-001 / SEC-HIGH-001)
|
|
51
|
+
|
|
52
|
+
| Threat | Mitigation | Tracked in |
|
|
53
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
54
|
+
| Compromised third-party `uses:` ref | All third-party Actions SHA-pinned (FR-031, M9 mirror gate). Dependabot opens PRs for tag→SHA rotation. | `installer/verify-mirror.sh` M9 |
|
|
55
|
+
| Untrusted YAML in `.apm/pipelines/**` deserialising arbitrary code | `yaml.load` pinned to `CORE_SCHEMA`; tag-aware loaders banned. Negative test: `engine/tests/api-version.test.js` `!!js/function` case. | FR-013, T-10 |
|
|
56
|
+
| Pipeline declares `apiVersion` newer than the pinned engine | `assertApiVersionSupported` fails fast with a remediation message naming both versions. | FR-013, T-10 |
|
|
57
|
+
| Workflow run with `permissions: write-all` (CWE-732 over-grant) | `installer/init.sh --upgrade` refuses to widen `permissions:` blocks; engine itself requests nothing. | FR-024, SEC-MED-002, T-20 |
|
|
58
|
+
| Long-lived `NPM_TOKEN` in repo secrets | Release workflow uses **OIDC trusted publishing + `npm publish --provenance`**; no `NPM_TOKEN` ever written to env. | SEC-HIGH-001, T-12 |
|
|
59
|
+
| Tampered release artefact | Signed `v*` tags + SLSA build provenance attached by `npm publish --provenance`. Verifying key documented in `engine/RELEASING.md`. | SC-009, SEC-MED-004, T-12, T-23 |
|
|
60
|
+
| Engine-bundle drift (unreviewed `dist/` change) | `engine-build-gate.yml` rebuilds via `ncc` on every PR touching `engine/**` and fails if `git diff --quiet engine/dist/` is non-empty. | FR-009, T-09 |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 4. Reporting a vulnerability
|
|
65
|
+
|
|
66
|
+
Use GitHub's private vulnerability reporting on this repository
|
|
67
|
+
(<https://github.com/dmitry-nalivaika/quorumkit/security>) — do **not** open a
|
|
68
|
+
public issue. The Security Agent owns triage SLA and CVE assignment per
|
|
69
|
+
`SECURITY.md` at the repo root.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 5. Change-control rules
|
|
74
|
+
|
|
75
|
+
1. **Adding a permission scope to the table above** requires a paired
|
|
76
|
+
spec change (`specs/047-repo-topology/spec.md` or a successor) and
|
|
77
|
+
Security Agent sign-off on the PR.
|
|
78
|
+
2. **Removing a scope** requires a deprecation note in `CHANGELOG.md`
|
|
79
|
+
and a major-version bump if any documented consumer pipeline used it.
|
|
80
|
+
3. **Tag-aware YAML loading is forbidden** — any reintroduction of
|
|
81
|
+
`yaml.load(raw)` without `{ schema: yaml.CORE_SCHEMA }` (or stricter)
|
|
82
|
+
is a Security Agent veto.
|
package/action.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# QuorumKit Engine — Composite GitHub Action (Issue #47, ADR-047 amendment §1)
|
|
3
|
+
#
|
|
4
|
+
# Runtime: Node 20 (FR-008, AR-D §6).
|
|
5
|
+
# Bundle: dist/index.js — produced by `npm run build` (ncc) and committed
|
|
6
|
+
# (FR-009). CI re-runs the build and fails if `git diff --quiet
|
|
7
|
+
# dist/` is non-empty (engine-build-gate workflow).
|
|
8
|
+
#
|
|
9
|
+
# Inputs: none — the engine reads everything from the GitHub event payload
|
|
10
|
+
# and the consumer-side `.apm/` SoT directory (ADR-006 §3).
|
|
11
|
+
# Permissions: declared per-call by the consumer workflow; this Action does
|
|
12
|
+
# NOT request any. The minimum-viable scope is `contents: read`
|
|
13
|
+
# + `issues: write` + `pull-requests: write` (engine/SECURITY.md).
|
|
14
|
+
# =============================================================================
|
|
15
|
+
name: 'QuorumKit Engine'
|
|
16
|
+
description: 'Run the QuorumKit autonomous-agent orchestrator on a GitHub event payload.'
|
|
17
|
+
author: 'QuorumKit contributors'
|
|
18
|
+
|
|
19
|
+
branding:
|
|
20
|
+
icon: 'cpu'
|
|
21
|
+
color: 'purple'
|
|
22
|
+
|
|
23
|
+
runs:
|
|
24
|
+
using: 'node20'
|
|
25
|
+
main: 'dist/index.js'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export const id = 212;
|
|
2
|
+
export const ids = [212,563];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 3563:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ isRetryable: () => (/* binding */ isRetryable),
|
|
11
|
+
/* harmony export */ withRetry: () => (/* binding */ withRetry)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/**
|
|
14
|
+
* runtimes/_retry.js
|
|
15
|
+
* Bounded exponential-backoff retry helper shared by all runtime adapters
|
|
16
|
+
* (ADR-007 §8, FR-030).
|
|
17
|
+
*
|
|
18
|
+
* Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.
|
|
19
|
+
* Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).
|
|
20
|
+
* Non-retryable errors are rethrown immediately.
|
|
21
|
+
*
|
|
22
|
+
* Returns the function's resolved value plus the number of retries used,
|
|
23
|
+
* so the caller can record `runtime_retries` in the audit comment without
|
|
24
|
+
* counting the invocation as a separate loop-budget iteration.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const DEFAULT_OPTS = Object.freeze({
|
|
28
|
+
maxAttempts: 3,
|
|
29
|
+
baseMs: 2_000,
|
|
30
|
+
maxTotalMs: 30_000,
|
|
31
|
+
jitter: 0.25,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine whether an error is retryable.
|
|
36
|
+
* @param {any} err
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function isRetryable(err) {
|
|
40
|
+
if (!err) return false;
|
|
41
|
+
// Octokit / fetch error styles
|
|
42
|
+
if (err.status === 429) return true;
|
|
43
|
+
if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;
|
|
44
|
+
// Node fetch network errors
|
|
45
|
+
const code = err.code ?? err.cause?.code;
|
|
46
|
+
if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;
|
|
47
|
+
if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run `fn` with bounded exponential backoff.
|
|
53
|
+
*
|
|
54
|
+
* @template T
|
|
55
|
+
* @param {() => Promise<T>} fn
|
|
56
|
+
* @param {object} [options]
|
|
57
|
+
* @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }
|
|
58
|
+
* @returns {Promise<{ value: T, retries: number }>}
|
|
59
|
+
*/
|
|
60
|
+
async function withRetry(fn, options = {}) {
|
|
61
|
+
const opts = { ...DEFAULT_OPTS, ...options };
|
|
62
|
+
const clock = options.clock ?? defaultClock;
|
|
63
|
+
|
|
64
|
+
const start = clock.now();
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
let lastError;
|
|
67
|
+
|
|
68
|
+
while (attempt < opts.maxAttempts) {
|
|
69
|
+
try {
|
|
70
|
+
const value = await fn();
|
|
71
|
+
return { value, retries: attempt };
|
|
72
|
+
} catch (err) {
|
|
73
|
+
lastError = err;
|
|
74
|
+
attempt += 1;
|
|
75
|
+
if (!isRetryable(err) || attempt >= opts.maxAttempts) break;
|
|
76
|
+
|
|
77
|
+
const elapsed = clock.now() - start;
|
|
78
|
+
if (elapsed >= opts.maxTotalMs) break;
|
|
79
|
+
|
|
80
|
+
const exp = Math.pow(2, attempt - 1) * opts.baseMs;
|
|
81
|
+
const jitter = exp * opts.jitter * (Math.random() * 2 - 1);
|
|
82
|
+
const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));
|
|
83
|
+
await clock.sleep(wait);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw lastError;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultClock = {
|
|
91
|
+
now: () => Date.now(),
|
|
92
|
+
sleep: ms => new Promise(r => setTimeout(r, ms)),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/***/ }),
|
|
97
|
+
|
|
98
|
+
/***/ 1212:
|
|
99
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
100
|
+
|
|
101
|
+
__webpack_require__.r(__webpack_exports__);
|
|
102
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
103
|
+
/* harmony export */ KIND: () => (/* binding */ KIND),
|
|
104
|
+
/* harmony export */ invoke: () => (/* binding */ invoke),
|
|
105
|
+
/* harmony export */ requiredPermissions: () => (/* binding */ requiredPermissions)
|
|
106
|
+
/* harmony export */ });
|
|
107
|
+
/* harmony import */ var _retry_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3563);
|
|
108
|
+
/**
|
|
109
|
+
* runtimes/claude.js
|
|
110
|
+
* Adapter for the Claude (Anthropic) runtime kind (ADR-005, ADR-002).
|
|
111
|
+
*
|
|
112
|
+
* Dispatches the agent's Claude workflow file (`agent-<slug>.yml`) through
|
|
113
|
+
* the supplied client. Mirrors copilot.js with a smaller required-permissions
|
|
114
|
+
* set (no `models:` scope — Claude calls go to api.anthropic.com).
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
const KIND = 'claude';
|
|
120
|
+
|
|
121
|
+
const requiredPermissions = Object.freeze({
|
|
122
|
+
contents: 'read',
|
|
123
|
+
issues: 'write',
|
|
124
|
+
'pull-requests': 'write',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function resolveCredential(runtime, env = process.env) {
|
|
128
|
+
const ref = runtime.credential_ref;
|
|
129
|
+
if (!ref) {
|
|
130
|
+
const e = new Error('runtime-credential-missing');
|
|
131
|
+
e.code = 'runtime-credential-missing';
|
|
132
|
+
e.credential_ref = '(none declared)';
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
const value = env[ref];
|
|
136
|
+
if (!value) {
|
|
137
|
+
const e = new Error('runtime-credential-missing');
|
|
138
|
+
e.code = 'runtime-credential-missing';
|
|
139
|
+
e.credential_ref = ref;
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function invoke(context) {
|
|
146
|
+
const env = context.env ?? process.env;
|
|
147
|
+
resolveCredential(context.runtime, env);
|
|
148
|
+
|
|
149
|
+
const workflow = `agent-${context.agent}.yml`;
|
|
150
|
+
const dispatchRef = context.ref ?? 'main';
|
|
151
|
+
const inputs = {
|
|
152
|
+
issue_number: String(context.issueNumber),
|
|
153
|
+
run_id: context.runId ?? '',
|
|
154
|
+
step: context.step ?? '',
|
|
155
|
+
iteration: String(context.iteration ?? 1),
|
|
156
|
+
runtime: context.runtimeName ?? '',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const { retries } = await (0,_retry_js__WEBPACK_IMPORTED_MODULE_0__.withRetry)(
|
|
160
|
+
() => context.client.triggerWorkflow(context.owner, context.repo, workflow, dispatchRef, inputs),
|
|
161
|
+
{ clock: context.clock }
|
|
162
|
+
);
|
|
163
|
+
return { dispatched: true, retries, workflow };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/***/ })
|
|
168
|
+
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=212.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"212.index.js","mappings":";;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AChFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././orchestrator/runtimes/_retry.js",".././orchestrator/runtimes/claude.js"],"sourcesContent":["/**\n * runtimes/_retry.js\n * Bounded exponential-backoff retry helper shared by all runtime adapters\n * (ADR-007 §8, FR-030).\n *\n * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.\n * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).\n * Non-retryable errors are rethrown immediately.\n *\n * Returns the function's resolved value plus the number of retries used,\n * so the caller can record `runtime_retries` in the audit comment without\n * counting the invocation as a separate loop-budget iteration.\n */\n\nconst DEFAULT_OPTS = Object.freeze({\n maxAttempts: 3,\n baseMs: 2_000,\n maxTotalMs: 30_000,\n jitter: 0.25,\n});\n\n/**\n * Determine whether an error is retryable.\n * @param {any} err\n * @returns {boolean}\n */\nexport function isRetryable(err) {\n if (!err) return false;\n // Octokit / fetch error styles\n if (err.status === 429) return true;\n if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;\n // Node fetch network errors\n const code = err.code ?? err.cause?.code;\n if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;\n if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;\n return false;\n}\n\n/**\n * Run `fn` with bounded exponential backoff.\n *\n * @template T\n * @param {() => Promise<T>} fn\n * @param {object} [options]\n * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }\n * @returns {Promise<{ value: T, retries: number }>}\n */\nexport async function withRetry(fn, options = {}) {\n const opts = { ...DEFAULT_OPTS, ...options };\n const clock = options.clock ?? defaultClock;\n\n const start = clock.now();\n let attempt = 0;\n let lastError;\n\n while (attempt < opts.maxAttempts) {\n try {\n const value = await fn();\n return { value, retries: attempt };\n } catch (err) {\n lastError = err;\n attempt += 1;\n if (!isRetryable(err) || attempt >= opts.maxAttempts) break;\n\n const elapsed = clock.now() - start;\n if (elapsed >= opts.maxTotalMs) break;\n\n const exp = Math.pow(2, attempt - 1) * opts.baseMs;\n const jitter = exp * opts.jitter * (Math.random() * 2 - 1);\n const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));\n await clock.sleep(wait);\n }\n }\n\n throw lastError;\n}\n\nconst defaultClock = {\n now: () => Date.now(),\n sleep: ms => new Promise(r => setTimeout(r, ms)),\n};\n","/**\n * runtimes/claude.js\n * Adapter for the Claude (Anthropic) runtime kind (ADR-005, ADR-002).\n *\n * Dispatches the agent's Claude workflow file (`agent-<slug>.yml`) through\n * the supplied client. Mirrors copilot.js with a smaller required-permissions\n * set (no `models:` scope — Claude calls go to api.anthropic.com).\n */\n\nimport { withRetry } from './_retry.js';\n\nexport const KIND = 'claude';\n\nexport const requiredPermissions = Object.freeze({\n contents: 'read',\n issues: 'write',\n 'pull-requests': 'write',\n});\n\nfunction resolveCredential(runtime, env = process.env) {\n const ref = runtime.credential_ref;\n if (!ref) {\n const e = new Error('runtime-credential-missing');\n e.code = 'runtime-credential-missing';\n e.credential_ref = '(none declared)';\n throw e;\n }\n const value = env[ref];\n if (!value) {\n const e = new Error('runtime-credential-missing');\n e.code = 'runtime-credential-missing';\n e.credential_ref = ref;\n throw e;\n }\n return value;\n}\n\nexport async function invoke(context) {\n const env = context.env ?? process.env;\n resolveCredential(context.runtime, env);\n\n const workflow = `agent-${context.agent}.yml`;\n const dispatchRef = context.ref ?? 'main';\n const inputs = {\n issue_number: String(context.issueNumber),\n run_id: context.runId ?? '',\n step: context.step ?? '',\n iteration: String(context.iteration ?? 1),\n runtime: context.runtimeName ?? '',\n };\n\n const { retries } = await withRetry(\n () => context.client.triggerWorkflow(context.owner, context.repo, workflow, dispatchRef, inputs),\n { clock: context.clock }\n );\n return { dispatched: true, retries, workflow };\n}\n"],"names":[],"sourceRoot":""}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const id = 563;
|
|
2
|
+
export const ids = [563];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 3563:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ isRetryable: () => (/* binding */ isRetryable),
|
|
11
|
+
/* harmony export */ withRetry: () => (/* binding */ withRetry)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/**
|
|
14
|
+
* runtimes/_retry.js
|
|
15
|
+
* Bounded exponential-backoff retry helper shared by all runtime adapters
|
|
16
|
+
* (ADR-007 §8, FR-030).
|
|
17
|
+
*
|
|
18
|
+
* Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.
|
|
19
|
+
* Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).
|
|
20
|
+
* Non-retryable errors are rethrown immediately.
|
|
21
|
+
*
|
|
22
|
+
* Returns the function's resolved value plus the number of retries used,
|
|
23
|
+
* so the caller can record `runtime_retries` in the audit comment without
|
|
24
|
+
* counting the invocation as a separate loop-budget iteration.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const DEFAULT_OPTS = Object.freeze({
|
|
28
|
+
maxAttempts: 3,
|
|
29
|
+
baseMs: 2_000,
|
|
30
|
+
maxTotalMs: 30_000,
|
|
31
|
+
jitter: 0.25,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine whether an error is retryable.
|
|
36
|
+
* @param {any} err
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function isRetryable(err) {
|
|
40
|
+
if (!err) return false;
|
|
41
|
+
// Octokit / fetch error styles
|
|
42
|
+
if (err.status === 429) return true;
|
|
43
|
+
if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;
|
|
44
|
+
// Node fetch network errors
|
|
45
|
+
const code = err.code ?? err.cause?.code;
|
|
46
|
+
if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;
|
|
47
|
+
if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run `fn` with bounded exponential backoff.
|
|
53
|
+
*
|
|
54
|
+
* @template T
|
|
55
|
+
* @param {() => Promise<T>} fn
|
|
56
|
+
* @param {object} [options]
|
|
57
|
+
* @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }
|
|
58
|
+
* @returns {Promise<{ value: T, retries: number }>}
|
|
59
|
+
*/
|
|
60
|
+
async function withRetry(fn, options = {}) {
|
|
61
|
+
const opts = { ...DEFAULT_OPTS, ...options };
|
|
62
|
+
const clock = options.clock ?? defaultClock;
|
|
63
|
+
|
|
64
|
+
const start = clock.now();
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
let lastError;
|
|
67
|
+
|
|
68
|
+
while (attempt < opts.maxAttempts) {
|
|
69
|
+
try {
|
|
70
|
+
const value = await fn();
|
|
71
|
+
return { value, retries: attempt };
|
|
72
|
+
} catch (err) {
|
|
73
|
+
lastError = err;
|
|
74
|
+
attempt += 1;
|
|
75
|
+
if (!isRetryable(err) || attempt >= opts.maxAttempts) break;
|
|
76
|
+
|
|
77
|
+
const elapsed = clock.now() - start;
|
|
78
|
+
if (elapsed >= opts.maxTotalMs) break;
|
|
79
|
+
|
|
80
|
+
const exp = Math.pow(2, attempt - 1) * opts.baseMs;
|
|
81
|
+
const jitter = exp * opts.jitter * (Math.random() * 2 - 1);
|
|
82
|
+
const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));
|
|
83
|
+
await clock.sleep(wait);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw lastError;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultClock = {
|
|
91
|
+
now: () => Date.now(),
|
|
92
|
+
sleep: ms => new Promise(r => setTimeout(r, ms)),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/***/ })
|
|
97
|
+
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=563.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"563.index.js","mappings":";;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././orchestrator/runtimes/_retry.js"],"sourcesContent":["/**\n * runtimes/_retry.js\n * Bounded exponential-backoff retry helper shared by all runtime adapters\n * (ADR-007 §8, FR-030).\n *\n * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.\n * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).\n * Non-retryable errors are rethrown immediately.\n *\n * Returns the function's resolved value plus the number of retries used,\n * so the caller can record `runtime_retries` in the audit comment without\n * counting the invocation as a separate loop-budget iteration.\n */\n\nconst DEFAULT_OPTS = Object.freeze({\n maxAttempts: 3,\n baseMs: 2_000,\n maxTotalMs: 30_000,\n jitter: 0.25,\n});\n\n/**\n * Determine whether an error is retryable.\n * @param {any} err\n * @returns {boolean}\n */\nexport function isRetryable(err) {\n if (!err) return false;\n // Octokit / fetch error styles\n if (err.status === 429) return true;\n if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;\n // Node fetch network errors\n const code = err.code ?? err.cause?.code;\n if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;\n if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;\n return false;\n}\n\n/**\n * Run `fn` with bounded exponential backoff.\n *\n * @template T\n * @param {() => Promise<T>} fn\n * @param {object} [options]\n * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }\n * @returns {Promise<{ value: T, retries: number }>}\n */\nexport async function withRetry(fn, options = {}) {\n const opts = { ...DEFAULT_OPTS, ...options };\n const clock = options.clock ?? defaultClock;\n\n const start = clock.now();\n let attempt = 0;\n let lastError;\n\n while (attempt < opts.maxAttempts) {\n try {\n const value = await fn();\n return { value, retries: attempt };\n } catch (err) {\n lastError = err;\n attempt += 1;\n if (!isRetryable(err) || attempt >= opts.maxAttempts) break;\n\n const elapsed = clock.now() - start;\n if (elapsed >= opts.maxTotalMs) break;\n\n const exp = Math.pow(2, attempt - 1) * opts.baseMs;\n const jitter = exp * opts.jitter * (Math.random() * 2 - 1);\n const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));\n await clock.sleep(wait);\n }\n }\n\n throw lastError;\n}\n\nconst defaultClock = {\n now: () => Date.now(),\n sleep: ms => new Promise(r => setTimeout(r, ms)),\n};\n"],"names":[],"sourceRoot":""}
|