projecta-rrr 1.24.4 → 1.24.6
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/AGENTS.md +1 -1
- package/CHANGELOG.md +21 -0
- package/commands/rrr/provision-coordinator.md +7 -2
- package/package.json +1 -1
- package/rrr/lib/team-mode/manager.js +288 -8
- package/scripts/rrr-team-mode.js +20 -3
package/AGENTS.md
CHANGED
|
@@ -96,4 +96,4 @@ The RRR loop follows five steps:
|
|
|
96
96
|
- Use `$rrr-help` for full skill catalogue
|
|
97
97
|
- Trigger phrases are case-sensitive: `$rrr-plan-phase` not `$rrr-PlanPhase`
|
|
98
98
|
|
|
99
|
-
<!-- generated: 2026-05-
|
|
99
|
+
<!-- generated: 2026-05-13T03:49:42.852Z | source: commands/rrr/*.md | count: 59 skills -->
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to RRR will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.24.6] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Infisical machine-identity auth mode** — `$rrr-provision-coordinator --auth-mode infisical-machine-identity --infisical-project-id <id> --infisical-env <env>` writes a coordinator contract that fetches secrets at boot from Infisical via Universal Auth. Required runtime secrets become `RRR_INFI_CID` + `RRR_INFI_CS` only; Anthropic and GitHub tokens live in Infisical and rotate centrally.
|
|
11
|
+
- **Infisical Sprite bootstrap** — `$rrr-provision-sprites --apply --infisical-project-id <id>` installs Infisical CLI inside each Sprite, writes machine-identity creds, and wires `~/.profile` to fetch `ANTHROPIC_API_KEY` on every login shell. Uses `RRR_INFI_CID` + `RRR_INFI_CS` from the provisioning host's environment.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Default coordinator auth mode** — `infisical-machine-identity` is now the default (was `claude-code-subscription`). The subscription path remains available via `--auth-mode claude-code-subscription`.
|
|
15
|
+
- **Coordinator README** — generated README emits the Infisical install + login + secret-fetch boot snippet when in Infisical mode; falls back to credentials-file injection or AI Gateway when in their respective modes.
|
|
16
|
+
- **`$rrr-coordinator-status`** — checks for `RRR_INFI_CID`/`RRR_INFI_CS` env and `infisical --version` when in Infisical mode.
|
|
17
|
+
|
|
18
|
+
## [1.24.5] - 2026-05-13
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Coordinator auth modes** — `$rrr-provision-coordinator` now writes an `auth` block with `mode: claude-code-subscription` (default) or `mode: anthropic-api-key`. Subscription mode uses `~/.claude/.credentials.json` injected via the Cloudflare Secret `CLAUDE_CREDENTIALS_JSON`, eliminating the need for `ANTHROPIC_API_KEY` on the coordinator.
|
|
22
|
+
- **Cloudflare AI Gateway option** — when `--auth-mode anthropic-api-key`, optional `--ai-gateway-account-id <id> --ai-gateway-id <id> [--ai-gateway-authenticated]` flags wire `ANTHROPIC_BASE_URL` to a gateway URL and add `CF_AI_GATEWAY_TOKEN` to required secrets when authenticated.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Coordinator README** — generated README now includes the sandbox boot snippet that writes `CLAUDE_CREDENTIALS_JSON` to `~/.claude/.credentials.json`, and only documents AI Gateway when API-key mode is selected.
|
|
26
|
+
- **`$rrr-coordinator-status`** — checks for `~/.claude/.credentials.json` or `CLAUDE_CREDENTIALS_JSON` env when subscription mode is active; falls back to API key / AI Gateway env checks otherwise.
|
|
27
|
+
|
|
7
28
|
## [1.24.4] - 2026-05-12
|
|
8
29
|
|
|
9
30
|
### Fixed
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rrr:provision-coordinator
|
|
3
3
|
description: Create the Cloudflare coordinator contract for RRR team mode integration review
|
|
4
|
-
argument-hint: "[--provider cloudflare-sandbox] [--name <coordinator-name>]"
|
|
4
|
+
argument-hint: "[--auth-mode claude-code-subscription|anthropic-api-key] [--ai-gateway-account-id <id> --ai-gateway-id <id> [--ai-gateway-authenticated]] [--provider cloudflare-sandbox] [--name <coordinator-name>]"
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Write
|
|
@@ -32,8 +32,13 @@ Prepare the repo's coordinator contract so a reusable Cloudflare Claude Code/San
|
|
|
32
32
|
|
|
33
33
|
</process>
|
|
34
34
|
|
|
35
|
+
<auth_modes>
|
|
36
|
+
- `claude-code-subscription` (default) — coordinator runs Claude Code authenticated by an Anthropic subscription. Store the contents of `~/.claude/.credentials.json` as the Cloudflare Secret `CLAUDE_CREDENTIALS_JSON`; sandbox boot writes it to disk so Claude Code resumes the session. No `ANTHROPIC_API_KEY` required.
|
|
37
|
+
- `anthropic-api-key` — coordinator uses `ANTHROPIC_API_KEY` (per-token billing). Pair with `--ai-gateway-account-id <id> --ai-gateway-id <id>` to route Anthropic traffic through Cloudflare AI Gateway for caching, rate limits, and observability. Add `--ai-gateway-authenticated` if the gateway requires `cf-aig-authorization: Bearer $CF_AI_GATEWAY_TOKEN`.
|
|
38
|
+
</auth_modes>
|
|
39
|
+
|
|
35
40
|
<constraints>
|
|
36
41
|
- Coordinator is report-first; never auto-merge to the base branch.
|
|
37
42
|
- Developer work remains in team Sprites and team branches.
|
|
38
|
-
- Do not store secrets in planning files.
|
|
43
|
+
- Do not store secrets in planning files — `CLAUDE_CREDENTIALS_JSON` and AI Gateway tokens belong in Cloudflare Secrets.
|
|
39
44
|
</constraints>
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const { execFileSync } = require('child_process');
|
|
6
7
|
const { resolvePlanningContext, requireTeamName } = require('./state-router');
|
|
@@ -200,6 +201,9 @@ function buildSpriteProvisionPlan(projectRoot, options) {
|
|
|
200
201
|
const workdir = spriteWorkdir(config, runtime);
|
|
201
202
|
const selectedTeams = normalizeList(opts.teams || opts.team);
|
|
202
203
|
const teams = selectedTeams.length > 0 ? selectedTeams : Object.keys(config.teams || {});
|
|
204
|
+
const infisicalProjectId = opts.infisicalProjectId || null;
|
|
205
|
+
const infisicalEnv = opts.infisicalEnv || 'dev';
|
|
206
|
+
const infisicalEnabled = !!infisicalProjectId;
|
|
203
207
|
const rows = [];
|
|
204
208
|
|
|
205
209
|
for (const rawTeam of teams) {
|
|
@@ -215,6 +219,34 @@ function buildSpriteProvisionPlan(projectRoot, options) {
|
|
|
215
219
|
: `git clone <repo-url> ${workdir}`;
|
|
216
220
|
const checkoutCommand = `gh auth setup-git >/dev/null 2>&1 || true; git fetch origin && git checkout -B ${teamConfig.branch} origin/${config.github.base_branch}`;
|
|
217
221
|
|
|
222
|
+
const loaderScript = [
|
|
223
|
+
'[ -f ~/.infisical-machine-creds ] && source ~/.infisical-machine-creds',
|
|
224
|
+
'if [ -n "$RRR_INFI_CID" ] && [ -n "$RRR_INFI_CS" ]; then',
|
|
225
|
+
' _IT=$(infisical login --method=universal-auth --client-id="$RRR_INFI_CID" --client-secret="$RRR_INFI_CS" --plain --silent 2>/dev/null)',
|
|
226
|
+
' if [ -n "$_IT" ]; then',
|
|
227
|
+
' export ANTHROPIC_API_KEY=$(infisical secrets get ANTHROPIC_API_KEY --token="$_IT" --projectId="$RRR_INFI_PROJ" --env="${RRR_INFI_ENV:-dev}" --plain --silent 2>/dev/null)',
|
|
228
|
+
' unset _IT',
|
|
229
|
+
' fi',
|
|
230
|
+
'fi'
|
|
231
|
+
].join('\n');
|
|
232
|
+
const credsScript = [
|
|
233
|
+
'umask 077 && cat > ~/.infisical-machine-creds <<EOF',
|
|
234
|
+
'export RRR_INFI_CID="$RRR_INFI_CID"',
|
|
235
|
+
'export RRR_INFI_CS="$RRR_INFI_CS"',
|
|
236
|
+
'export RRR_INFI_PROJ="$RRR_INFI_PROJ"',
|
|
237
|
+
'export RRR_INFI_ENV="$RRR_INFI_ENV"',
|
|
238
|
+
'EOF'
|
|
239
|
+
].join('\n');
|
|
240
|
+
const writeLoaderCommand = `cat > ~/.infisical-load.sh <<'LOADER'\n${loaderScript}\nLOADER\ngrep -q "infisical-load.sh" ~/.profile || echo "[ -f ~/.infisical-load.sh ] && source ~/.infisical-load.sh" >> ~/.profile`;
|
|
241
|
+
const infisicalCommands = infisicalEnabled ? [
|
|
242
|
+
'# Install Infisical CLI for runtime secret fetch',
|
|
243
|
+
`sprite exec -s ${teamConfig.sprite} -- bash -lc 'command -v infisical || (curl -1sLf "https://artifacts-cli.infisical.com/setup.deb.sh" | sudo -E bash && sudo apt-get install -y infisical)'`,
|
|
244
|
+
'# Write machine-identity credentials (read-only, mode 600)',
|
|
245
|
+
`sprite exec -s ${teamConfig.sprite} --env "RRR_INFI_CID=$RRR_INFI_CID,RRR_INFI_CS=$RRR_INFI_CS,RRR_INFI_PROJ=${infisicalProjectId},RRR_INFI_ENV=${infisicalEnv}" -- bash -lc ${JSON.stringify(credsScript)}`,
|
|
246
|
+
'# Loader fetches ANTHROPIC_API_KEY on every login shell',
|
|
247
|
+
`sprite exec -s ${teamConfig.sprite} -- bash -lc ${JSON.stringify(writeLoaderCommand)}`
|
|
248
|
+
] : [];
|
|
249
|
+
|
|
218
250
|
rows.push({
|
|
219
251
|
team,
|
|
220
252
|
github_team: teamConfig.github_team,
|
|
@@ -225,6 +257,7 @@ function buildSpriteProvisionPlan(projectRoot, options) {
|
|
|
225
257
|
repo,
|
|
226
258
|
setup,
|
|
227
259
|
checks,
|
|
260
|
+
infisical: infisicalEnabled ? { project_id: infisicalProjectId, env: infisicalEnv } : null,
|
|
228
261
|
commands: [
|
|
229
262
|
`sprite create ${teamConfig.sprite} --skip-console`,
|
|
230
263
|
`sprite exec -s ${teamConfig.sprite} -- mkdir -p /home/sprite/work`,
|
|
@@ -233,12 +266,13 @@ function buildSpriteProvisionPlan(projectRoot, options) {
|
|
|
233
266
|
`sprite exec -s ${teamConfig.sprite} --dir ${workdir} --env GITHUB_TOKEN=$GITHUB_TOKEN -- bash -lc '${checkoutCommand}'`,
|
|
234
267
|
`sprite exec -s ${teamConfig.sprite} --dir ${workdir} -- npx projecta-rrr@latest --global --yes`,
|
|
235
268
|
...setup.map(cmd => `sprite exec -s ${teamConfig.sprite} --dir ${workdir} -- bash -lc '${cmd}'`),
|
|
269
|
+
...infisicalCommands,
|
|
236
270
|
`sprite checkpoint create -s ${teamConfig.sprite} --comment "rrr clean-ready ${config.milestone} ${team}"`
|
|
237
271
|
]
|
|
238
272
|
});
|
|
239
273
|
}
|
|
240
274
|
|
|
241
|
-
return { config, runtime, workdir, rows };
|
|
275
|
+
return { config, runtime, workdir, rows, infisical: infisicalEnabled ? { project_id: infisicalProjectId, env: infisicalEnv } : null };
|
|
242
276
|
}
|
|
243
277
|
|
|
244
278
|
function renderSpriteProvisionPlan(plan) {
|
|
@@ -415,6 +449,31 @@ function applySpriteProvisionPlan(projectRoot, options) {
|
|
|
415
449
|
const setup = execCapture('sprite', ['exec', '-s', row.sprite, '--dir', row.workdir, '--', 'bash', '-lc', command]);
|
|
416
450
|
teamResults.push({ step: `setup: ${command}`, ...setup });
|
|
417
451
|
}
|
|
452
|
+
if (row.infisical && process.env.RRR_INFI_CID && process.env.RRR_INFI_CS) {
|
|
453
|
+
const cid = process.env.RRR_INFI_CID;
|
|
454
|
+
const cs = process.env.RRR_INFI_CS;
|
|
455
|
+
const proj = row.infisical.project_id;
|
|
456
|
+
const env = row.infisical.env;
|
|
457
|
+
const installInfi = execCapture('sprite', [
|
|
458
|
+
'exec', '-s', row.sprite, '--', 'bash', '-lc',
|
|
459
|
+
'command -v infisical || (curl -1sLf "https://artifacts-cli.infisical.com/setup.deb.sh" | sudo -E bash >/dev/null 2>&1 && sudo apt-get install -y infisical >/dev/null 2>&1) && infisical --version'
|
|
460
|
+
]);
|
|
461
|
+
teamResults.push({ step: 'infisical-install', ...installInfi });
|
|
462
|
+
const writeCreds = execCapture('sprite', [
|
|
463
|
+
'exec', '-s', row.sprite,
|
|
464
|
+
'--env', `RRR_INFI_CID=${cid},RRR_INFI_CS=${cs},RRR_INFI_PROJ=${proj},RRR_INFI_ENV=${env}`,
|
|
465
|
+
'--', 'bash', '-lc',
|
|
466
|
+
'umask 077 && cat > ~/.infisical-machine-creds <<EOF\nexport RRR_INFI_CID="$RRR_INFI_CID"\nexport RRR_INFI_CS="$RRR_INFI_CS"\nexport RRR_INFI_PROJ="$RRR_INFI_PROJ"\nexport RRR_INFI_ENV="$RRR_INFI_ENV"\nEOF'
|
|
467
|
+
]);
|
|
468
|
+
teamResults.push({ step: 'infisical-creds', ...writeCreds });
|
|
469
|
+
const writeLoader = execCapture('sprite', [
|
|
470
|
+
'exec', '-s', row.sprite, '--', 'bash', '-lc',
|
|
471
|
+
'cat > ~/.infisical-load.sh <<\'LOADER\'\n[ -f ~/.infisical-machine-creds ] && source ~/.infisical-machine-creds\nif [ -n "$RRR_INFI_CID" ] && [ -n "$RRR_INFI_CS" ]; then\n _IT=$(infisical login --method=universal-auth --client-id="$RRR_INFI_CID" --client-secret="$RRR_INFI_CS" --plain --silent 2>/dev/null)\n if [ -n "$_IT" ]; then\n export ANTHROPIC_API_KEY=$(infisical secrets get ANTHROPIC_API_KEY --token="$_IT" --projectId="$RRR_INFI_PROJ" --env="${RRR_INFI_ENV:-dev}" --plain --silent 2>/dev/null)\n unset _IT\n fi\nfi\nLOADER\ngrep -q "infisical-load.sh" ~/.profile || echo "[ -f ~/.infisical-load.sh ] && source ~/.infisical-load.sh" >> ~/.profile'
|
|
472
|
+
]);
|
|
473
|
+
teamResults.push({ step: 'infisical-loader', ...writeLoader });
|
|
474
|
+
} else if (row.infisical) {
|
|
475
|
+
teamResults.push({ step: 'infisical-skip', ok: false, output: 'infisical bootstrap requested but RRR_INFI_CID/RRR_INFI_CS not set in apply environment' });
|
|
476
|
+
}
|
|
418
477
|
const readyForCheckpoint = teamResults.every(result => result.ok);
|
|
419
478
|
const checkpoint = readyForCheckpoint
|
|
420
479
|
? execCapture('sprite', [
|
|
@@ -514,6 +573,86 @@ function coordinatorConfigPath(projectRoot) {
|
|
|
514
573
|
return path.join(projectRoot, '.planning', 'coordinator', 'COORDINATOR.json');
|
|
515
574
|
}
|
|
516
575
|
|
|
576
|
+
function buildAiGatewayBlock(opts) {
|
|
577
|
+
const accountId = opts.aiGatewayAccountId || opts.cfAccountId || null;
|
|
578
|
+
const gatewayId = opts.aiGatewayId || null;
|
|
579
|
+
if (!accountId || !gatewayId) return null;
|
|
580
|
+
const authenticated = opts.aiGatewayAuthenticated === true || opts.aiGatewayAuthenticated === 'true';
|
|
581
|
+
const baseUrl = `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`;
|
|
582
|
+
return {
|
|
583
|
+
provider: 'cloudflare-ai-gateway',
|
|
584
|
+
account_id: accountId,
|
|
585
|
+
gateway_id: gatewayId,
|
|
586
|
+
anthropic_base_url: baseUrl,
|
|
587
|
+
authenticated,
|
|
588
|
+
runtime_env: { ANTHROPIC_BASE_URL: baseUrl },
|
|
589
|
+
notes: [
|
|
590
|
+
'Only used when auth_mode = anthropic-api-key.',
|
|
591
|
+
'Set ANTHROPIC_BASE_URL to route Anthropic API calls through the gateway.',
|
|
592
|
+
authenticated
|
|
593
|
+
? 'Authenticated gateway: send `cf-aig-authorization: Bearer $CF_AI_GATEWAY_TOKEN` header on every request.'
|
|
594
|
+
: 'Unauthenticated gateway: ANTHROPIC_API_KEY is the only credential; gateway URL is the access boundary.'
|
|
595
|
+
]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function buildAuthBlock(opts) {
|
|
600
|
+
const mode = opts.authMode || 'infisical-machine-identity';
|
|
601
|
+
if (mode === 'infisical-machine-identity') {
|
|
602
|
+
const projectId = opts.infisicalProjectId || null;
|
|
603
|
+
const env = opts.infisicalEnv || 'dev';
|
|
604
|
+
const domain = opts.infisicalDomain || 'https://app.infisical.com';
|
|
605
|
+
const secrets = opts.infisicalSecrets && opts.infisicalSecrets.length > 0
|
|
606
|
+
? opts.infisicalSecrets
|
|
607
|
+
: ['ANTHROPIC_API_KEY', 'GITHUB_TOKEN'];
|
|
608
|
+
return {
|
|
609
|
+
mode,
|
|
610
|
+
description: 'Coordinator fetches secrets at boot from Infisical via Universal Auth (machine identity).',
|
|
611
|
+
infisical: {
|
|
612
|
+
domain,
|
|
613
|
+
project_id: projectId,
|
|
614
|
+
env,
|
|
615
|
+
secrets
|
|
616
|
+
},
|
|
617
|
+
required_runtime_env: ['RRR_INFI_CID', 'RRR_INFI_CS'],
|
|
618
|
+
required_secrets: ['RRR_INFI_CID', 'RRR_INFI_CS'],
|
|
619
|
+
notes: [
|
|
620
|
+
'RRR_INFI_CID + RRR_INFI_CS are Infisical Universal Auth client credentials (stored as Cloudflare Secrets / Wrangler secrets).',
|
|
621
|
+
'Boot script calls `infisical login --method=universal-auth` then `infisical secrets get` for each declared secret.',
|
|
622
|
+
'Anthropic, GitHub, and any future tokens rotate centrally in Infisical with no coordinator redeploy.'
|
|
623
|
+
]
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (mode === 'claude-code-subscription') {
|
|
627
|
+
return {
|
|
628
|
+
mode,
|
|
629
|
+
description: 'Coordinator uses Claude Code with an Anthropic subscription (Max/Pro). No ANTHROPIC_API_KEY required.',
|
|
630
|
+
credentials_path: '~/.claude/.credentials.json',
|
|
631
|
+
injection: [
|
|
632
|
+
'Persist the credentials file once (after a one-time `claude login`).',
|
|
633
|
+
'For a Cloudflare sandbox: write the stored credentials JSON to ~/.claude/.credentials.json on container start (from a CF Secret, KV, or D1 row).',
|
|
634
|
+
'For a Sprite: run `claude login` once interactively, then `sprite checkpoint create` to capture the authenticated state.'
|
|
635
|
+
],
|
|
636
|
+
required_secrets: ['GITHUB_TOKEN', 'CLAUDE_CREDENTIALS_JSON'],
|
|
637
|
+
notes: [
|
|
638
|
+
'CLAUDE_CREDENTIALS_JSON holds the contents of ~/.claude/.credentials.json — store as a CF Secret, not in repo.',
|
|
639
|
+
'Subscription billing applies; no per-token API cost.'
|
|
640
|
+
]
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (mode === 'anthropic-api-key') {
|
|
644
|
+
return {
|
|
645
|
+
mode,
|
|
646
|
+
description: 'Coordinator authenticates Claude Code with ANTHROPIC_API_KEY (per-token billing).',
|
|
647
|
+
required_secrets: ['ANTHROPIC_API_KEY', 'GITHUB_TOKEN'],
|
|
648
|
+
notes: [
|
|
649
|
+
'Optional: pair with Cloudflare AI Gateway via --ai-gateway-account-id/--ai-gateway-id for caching, rate limits, and observability.'
|
|
650
|
+
]
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
throw new Error(`unknown auth_mode: ${mode}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
517
656
|
function writeCoordinatorProvision(options) {
|
|
518
657
|
const opts = options || {};
|
|
519
658
|
const projectRoot = opts.projectRoot || process.cwd();
|
|
@@ -521,8 +660,13 @@ function writeCoordinatorProvision(options) {
|
|
|
521
660
|
const runtime = readRuntimeManifest(projectRoot);
|
|
522
661
|
const dir = path.join(projectRoot, '.planning', 'coordinator');
|
|
523
662
|
ensureDir(dir);
|
|
663
|
+
const auth = buildAuthBlock(opts);
|
|
664
|
+
const aiGateway = auth.mode === 'anthropic-api-key' ? buildAiGatewayBlock(opts) : null;
|
|
665
|
+
const requiredSecrets = [...auth.required_secrets];
|
|
666
|
+
if (aiGateway && aiGateway.authenticated) requiredSecrets.push('CF_AI_GATEWAY_TOKEN');
|
|
667
|
+
|
|
524
668
|
const config = {
|
|
525
|
-
schema_version:
|
|
669
|
+
schema_version: 2,
|
|
526
670
|
provider: opts.provider || 'cloudflare-sandbox',
|
|
527
671
|
name: opts.name || `rrr-coordinator-${repoNameFor(teamConfig).toLowerCase()}`,
|
|
528
672
|
generated_at: new Date().toISOString(),
|
|
@@ -532,7 +676,9 @@ function writeCoordinatorProvision(options) {
|
|
|
532
676
|
integration_branch: teamConfig.github.integration_branch,
|
|
533
677
|
runtime_manifest: path.relative(projectRoot, runtimeManifestPath(projectRoot)),
|
|
534
678
|
report_first: true,
|
|
535
|
-
|
|
679
|
+
auth,
|
|
680
|
+
ai_gateway: aiGateway,
|
|
681
|
+
required_secrets: requiredSecrets,
|
|
536
682
|
responsibilities: [
|
|
537
683
|
'clone repository into ephemeral sandbox',
|
|
538
684
|
'merge team branches into integration branch',
|
|
@@ -542,6 +688,7 @@ function writeCoordinatorProvision(options) {
|
|
|
542
688
|
]
|
|
543
689
|
};
|
|
544
690
|
writeJson(coordinatorConfigPath(projectRoot), config);
|
|
691
|
+
|
|
545
692
|
const readme = [
|
|
546
693
|
`# ${config.name}`,
|
|
547
694
|
'',
|
|
@@ -554,11 +701,98 @@ function writeCoordinatorProvision(options) {
|
|
|
554
701
|
`Base branch: ${config.base_branch}`,
|
|
555
702
|
`Integration branch: ${config.integration_branch}`,
|
|
556
703
|
`Runtime manifest: ${config.runtime_manifest}`,
|
|
704
|
+
'',
|
|
705
|
+
'## Auth',
|
|
706
|
+
'',
|
|
707
|
+
`Mode: \`${auth.mode}\``,
|
|
708
|
+
'',
|
|
709
|
+
auth.description
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
if (auth.mode === 'infisical-machine-identity') {
|
|
713
|
+
const infi = auth.infisical;
|
|
714
|
+
const fetches = (infi.secrets || []).map(s =>
|
|
715
|
+
`export ${s}=$(infisical secrets get ${s} --token="$_IT" --projectId="${infi.project_id || '<INFISICAL_PROJECT_ID>'}" --env="${infi.env}" --plain --silent)`
|
|
716
|
+
);
|
|
717
|
+
readme.push(
|
|
718
|
+
'',
|
|
719
|
+
'### Infisical machine-identity bootstrap',
|
|
720
|
+
'',
|
|
721
|
+
`- Domain: ${infi.domain}`,
|
|
722
|
+
`- Project ID: ${infi.project_id || '(unset — pass --infisical-project-id)'}`,
|
|
723
|
+
`- Env: ${infi.env}`,
|
|
724
|
+
`- Fetched secrets: ${(infi.secrets || []).join(', ') || '(none)'}`,
|
|
725
|
+
'',
|
|
726
|
+
'Cloudflare sandbox boot snippet:',
|
|
727
|
+
'',
|
|
728
|
+
'```bash',
|
|
729
|
+
'# Install Infisical CLI (one-time, ideally baked into image)',
|
|
730
|
+
'curl -1sLf "https://artifacts-cli.infisical.com/setup.deb.sh" | sudo -E bash',
|
|
731
|
+
'sudo apt-get install -y infisical',
|
|
732
|
+
'',
|
|
733
|
+
'# Exchange machine-identity credentials for an access token',
|
|
734
|
+
'_IT=$(infisical login --method=universal-auth --client-id="$RRR_INFI_CID" --client-secret="$RRR_INFI_CS" --plain --silent)',
|
|
735
|
+
'',
|
|
736
|
+
'# Fetch and export each declared secret',
|
|
737
|
+
...fetches,
|
|
738
|
+
'unset _IT',
|
|
739
|
+
'',
|
|
740
|
+
'claude --version # picks up ANTHROPIC_API_KEY automatically',
|
|
741
|
+
'```'
|
|
742
|
+
);
|
|
743
|
+
} else if (auth.mode === 'claude-code-subscription') {
|
|
744
|
+
readme.push(
|
|
745
|
+
'',
|
|
746
|
+
'### Credentials injection',
|
|
747
|
+
'',
|
|
748
|
+
...auth.injection.map(line => `- ${line}`),
|
|
749
|
+
'',
|
|
750
|
+
'Cloudflare sandbox boot snippet:',
|
|
751
|
+
'',
|
|
752
|
+
'```bash',
|
|
753
|
+
'mkdir -p ~/.claude',
|
|
754
|
+
'printf "%s" "$CLAUDE_CREDENTIALS_JSON" > ~/.claude/.credentials.json',
|
|
755
|
+
'chmod 600 ~/.claude/.credentials.json',
|
|
756
|
+
'claude --version # verifies authenticated',
|
|
757
|
+
'```'
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
readme.push(
|
|
557
762
|
'',
|
|
558
763
|
'## Required Secrets',
|
|
559
764
|
'',
|
|
560
|
-
|
|
561
|
-
|
|
765
|
+
...requiredSecrets.map(s => `- \`${s}\``)
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
if (aiGateway) {
|
|
769
|
+
readme.push(
|
|
770
|
+
'',
|
|
771
|
+
'## Cloudflare AI Gateway (optional)',
|
|
772
|
+
'',
|
|
773
|
+
`Account: ${aiGateway.account_id}`,
|
|
774
|
+
`Gateway: ${aiGateway.gateway_id}`,
|
|
775
|
+
`Anthropic base URL: ${aiGateway.anthropic_base_url}`,
|
|
776
|
+
`Authenticated: ${aiGateway.authenticated ? 'yes' : 'no'}`,
|
|
777
|
+
'',
|
|
778
|
+
'```bash',
|
|
779
|
+
`export ANTHROPIC_BASE_URL="${aiGateway.anthropic_base_url}"`,
|
|
780
|
+
'export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"',
|
|
781
|
+
aiGateway.authenticated
|
|
782
|
+
? '# header: cf-aig-authorization: Bearer $CF_AI_GATEWAY_TOKEN'
|
|
783
|
+
: '# unauthenticated gateway — URL is the access boundary',
|
|
784
|
+
'```'
|
|
785
|
+
);
|
|
786
|
+
} else if (auth.mode === 'anthropic-api-key') {
|
|
787
|
+
readme.push(
|
|
788
|
+
'',
|
|
789
|
+
'## Cloudflare AI Gateway',
|
|
790
|
+
'',
|
|
791
|
+
'Not configured. Re-run with `--ai-gateway-account-id <id> --ai-gateway-id <id>` to route Anthropic API traffic through Cloudflare AI Gateway.'
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
readme.push(
|
|
562
796
|
'',
|
|
563
797
|
'## Execution Contract',
|
|
564
798
|
'',
|
|
@@ -566,8 +800,9 @@ function writeCoordinatorProvision(options) {
|
|
|
566
800
|
'- GitHub remains the source of truth for branches, PRs, reviews, and checks.',
|
|
567
801
|
'- Coordinator is report-first: it writes reports and recommendations only; humans approve final merges.',
|
|
568
802
|
'- Developer work stays in Sprites/team branches.'
|
|
569
|
-
|
|
570
|
-
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
atomicWrite(path.join(dir, 'README.md'), `${readme.join('\n')}\n`);
|
|
571
806
|
return { config, runtime, dir };
|
|
572
807
|
}
|
|
573
808
|
|
|
@@ -579,8 +814,53 @@ function readCoordinatorStatus(projectRoot) {
|
|
|
579
814
|
checks.push({ name: 'runtime manifest', ok: fs.existsSync(runtimeManifestPath(root)), detail: path.relative(root, runtimeManifestPath(root)) });
|
|
580
815
|
const wrangler = execCapture('npx', ['wrangler', '--version'], { cwd: root });
|
|
581
816
|
checks.push({ name: 'wrangler available', ok: wrangler.ok, detail: wrangler.output || 'not available' });
|
|
582
|
-
checks.push({ name: 'ANTHROPIC_API_KEY present', ok: !!process.env.ANTHROPIC_API_KEY, detail: process.env.ANTHROPIC_API_KEY ? 'present' : 'missing locally' });
|
|
583
817
|
checks.push({ name: 'GITHUB_TOKEN present', ok: !!process.env.GITHUB_TOKEN, detail: process.env.GITHUB_TOKEN ? 'present' : 'missing locally' });
|
|
818
|
+
|
|
819
|
+
const authMode = config && config.auth ? config.auth.mode : (config && config.ai_gateway ? 'anthropic-api-key' : 'infisical-machine-identity');
|
|
820
|
+
if (authMode === 'infisical-machine-identity') {
|
|
821
|
+
checks.push({
|
|
822
|
+
name: 'RRR_INFI_CID present',
|
|
823
|
+
ok: !!process.env.RRR_INFI_CID,
|
|
824
|
+
detail: process.env.RRR_INFI_CID ? 'present' : 'missing locally (Infisical Universal Auth client ID)'
|
|
825
|
+
});
|
|
826
|
+
checks.push({
|
|
827
|
+
name: 'RRR_INFI_CS present',
|
|
828
|
+
ok: !!process.env.RRR_INFI_CS,
|
|
829
|
+
detail: process.env.RRR_INFI_CS ? 'present' : 'missing locally (Infisical Universal Auth client secret)'
|
|
830
|
+
});
|
|
831
|
+
const infiCli = execCapture('infisical', ['--version']);
|
|
832
|
+
checks.push({
|
|
833
|
+
name: 'infisical CLI available',
|
|
834
|
+
ok: infiCli.ok,
|
|
835
|
+
detail: infiCli.output || 'not installed locally; coordinator runtime must install it'
|
|
836
|
+
});
|
|
837
|
+
} else if (authMode === 'claude-code-subscription') {
|
|
838
|
+
const credsPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
839
|
+
const localCreds = fs.existsSync(credsPath);
|
|
840
|
+
checks.push({
|
|
841
|
+
name: 'Claude Code subscription credentials',
|
|
842
|
+
ok: localCreds || !!process.env.CLAUDE_CREDENTIALS_JSON,
|
|
843
|
+
detail: localCreds ? credsPath : (process.env.CLAUDE_CREDENTIALS_JSON ? 'CLAUDE_CREDENTIALS_JSON env present' : 'missing locally (run `claude login` or set CLAUDE_CREDENTIALS_JSON)')
|
|
844
|
+
});
|
|
845
|
+
} else {
|
|
846
|
+
checks.push({ name: 'ANTHROPIC_API_KEY present', ok: !!process.env.ANTHROPIC_API_KEY, detail: process.env.ANTHROPIC_API_KEY ? 'present' : 'missing locally' });
|
|
847
|
+
if (config && config.ai_gateway) {
|
|
848
|
+
const expectedBase = config.ai_gateway.anthropic_base_url;
|
|
849
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL;
|
|
850
|
+
checks.push({
|
|
851
|
+
name: 'ANTHROPIC_BASE_URL routes to AI Gateway',
|
|
852
|
+
ok: baseUrl === expectedBase,
|
|
853
|
+
detail: baseUrl ? (baseUrl === expectedBase ? expectedBase : `mismatch: ${baseUrl}`) : `missing locally (expected ${expectedBase})`
|
|
854
|
+
});
|
|
855
|
+
if (config.ai_gateway.authenticated) {
|
|
856
|
+
checks.push({
|
|
857
|
+
name: 'CF_AI_GATEWAY_TOKEN present',
|
|
858
|
+
ok: !!process.env.CF_AI_GATEWAY_TOKEN,
|
|
859
|
+
detail: process.env.CF_AI_GATEWAY_TOKEN ? 'present' : 'missing locally (authenticated gateway requires it)'
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
584
864
|
return { config, checks };
|
|
585
865
|
}
|
|
586
866
|
|
package/scripts/rrr-team-mode.js
CHANGED
|
@@ -73,7 +73,7 @@ Usage:
|
|
|
73
73
|
node scripts/rrr-team-mode.js runtime-init
|
|
74
74
|
node scripts/rrr-team-mode.js provision-sprites [--apply] [--teams team-runtime]
|
|
75
75
|
node scripts/rrr-team-mode.js sprite-status [--live]
|
|
76
|
-
node scripts/rrr-team-mode.js provision-coordinator
|
|
76
|
+
node scripts/rrr-team-mode.js provision-coordinator [--auth-mode claude-code-subscription|anthropic-api-key] [--ai-gateway-account-id <id> --ai-gateway-id <id> [--ai-gateway-authenticated]]
|
|
77
77
|
node scripts/rrr-team-mode.js coordinator-status
|
|
78
78
|
|
|
79
79
|
Notes:
|
|
@@ -189,7 +189,9 @@ function main() {
|
|
|
189
189
|
if (command === 'provision-sprites') {
|
|
190
190
|
const options = {
|
|
191
191
|
teams: args.teams,
|
|
192
|
-
team: args.team
|
|
192
|
+
team: args.team,
|
|
193
|
+
infisicalProjectId: args['infisical-project-id'],
|
|
194
|
+
infisicalEnv: args['infisical-env']
|
|
193
195
|
};
|
|
194
196
|
if (args.apply === true || args.apply === 'true') {
|
|
195
197
|
const result = applySpriteProvisionPlan(projectRoot, options);
|
|
@@ -224,10 +226,25 @@ function main() {
|
|
|
224
226
|
const result = writeCoordinatorProvision({
|
|
225
227
|
projectRoot,
|
|
226
228
|
provider: args.provider,
|
|
227
|
-
name: args.name
|
|
229
|
+
name: args.name,
|
|
230
|
+
authMode: args['auth-mode'] || args.authMode,
|
|
231
|
+
infisicalProjectId: args['infisical-project-id'],
|
|
232
|
+
infisicalEnv: args['infisical-env'],
|
|
233
|
+
infisicalDomain: args['infisical-domain'],
|
|
234
|
+
infisicalSecrets: splitCsv(args['infisical-secrets']),
|
|
235
|
+
aiGatewayAccountId: args['ai-gateway-account-id'] || args['cf-account-id'],
|
|
236
|
+
aiGatewayId: args['ai-gateway-id'],
|
|
237
|
+
aiGatewayAuthenticated: args['ai-gateway-authenticated'] === true || args['ai-gateway-authenticated'] === 'true'
|
|
228
238
|
});
|
|
229
239
|
console.log(`Wrote ${path.relative(projectRoot, path.join(result.dir, 'COORDINATOR.json'))}`);
|
|
230
240
|
console.log(`Wrote ${path.relative(projectRoot, path.join(result.dir, 'README.md'))}`);
|
|
241
|
+
console.log(`Auth mode: ${result.config.auth.mode}`);
|
|
242
|
+
if (result.config.auth && result.config.auth.infisical) {
|
|
243
|
+
console.log(`Infisical: ${result.config.auth.infisical.project_id || '(unset)'}@${result.config.auth.infisical.env}`);
|
|
244
|
+
}
|
|
245
|
+
if (result.config.ai_gateway) {
|
|
246
|
+
console.log(`AI Gateway: ${result.config.ai_gateway.anthropic_base_url}`);
|
|
247
|
+
}
|
|
231
248
|
return;
|
|
232
249
|
}
|
|
233
250
|
|