@agenthippo-ai/agenthippo 0.1.1 → 0.2.3-dataplane
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/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/webview/panel.js +290 -80
- package/out/analytics/sessionEvents.js +21 -9
- package/out/auth/brokerClient.js +1 -1
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +1 -1
- package/out/auth-broker/config.js +8 -3
- package/out/auth-broker/server.js +36 -0
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/deploy.js +49 -10
- package/out/cli/commands/gateway.js +54 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/serve.js +315 -55
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +195 -0
- package/out/cli/data-plane/index.js +137 -0
- package/out/cli/data-plane/registry.js +80 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +91 -0
- package/out/cli/deploy-init.js +1105 -335
- package/out/cli/idp-init.js +303 -0
- package/out/cli/index.js +37 -7
- package/out/cli/pack-loader.js +37 -2
- package/out/cli/runtime.js +10 -3
- package/out/cli/serve-hardening.js +60 -1
- package/out/cli/state-sync.js +16 -15
- package/out/engines/claude.js +26 -5
- package/out/engines/codex.js +24 -5
- package/out/engines/custom.js +46 -12
- package/out/extension.js +2 -0
- package/out/gateway/outboundSend.js +6 -6
- package/out/headless/index.js +24 -9
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +85 -67
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
- package/out/shared/models.js +1 -1
- package/out/shared/providers.js +2 -2
- package/out/test/analytics.test.js +42 -17
- package/out/test/auth-broker.test.js +53 -0
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +244 -0
- package/out/test/deploy-init.test.js +255 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +43 -3
- package/out/test/model-routing.test.js +133 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +80 -0
- package/out/test/serveClient.test.js +32 -0
- package/out/test/state-sync-peer.test.js +18 -5
- package/out/utils.js +27 -11
- package/package.json +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderIdpArtifacts = renderIdpArtifacts;
|
|
7
|
+
/**
|
|
8
|
+
* `agenthippo idp init` — generator for the central IdP stack (PLAN/deploy.md, "customer 0").
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: the org IdP used to be static files checked into this source repo
|
|
11
|
+
* (enterprise-deploy/idp/), which meant the actual identity routes could drift from
|
|
12
|
+
* renderIdentityLayer() (fixed once already) AND — the bigger problem — an enterprise
|
|
13
|
+
* customer standing up their own IdP had no way to get any of it at all, since they only
|
|
14
|
+
* receive the compiled `agenthippo` binary, never this repo. Generating it here, from the
|
|
15
|
+
* same shared identity/auth-env/Dockerfile renderers every `deploy init` target already
|
|
16
|
+
* uses, fixes both: it ships in the binary, and it can never drift from what agents expect.
|
|
17
|
+
*
|
|
18
|
+
* AgentHippo's own auth.agenthippo.ai instance is "customer 0" — stood up with this exact
|
|
19
|
+
* command, not a bespoke internal setup.
|
|
20
|
+
*/
|
|
21
|
+
const identity_1 = require("./deploy/layers/identity");
|
|
22
|
+
const deploy_init_1 = require("./deploy-init");
|
|
23
|
+
function resolveCookieDomain(ctx) {
|
|
24
|
+
if (ctx.cookieDomain?.trim()) {
|
|
25
|
+
return ctx.cookieDomain.trim();
|
|
26
|
+
}
|
|
27
|
+
const parts = ctx.hostname.split('.');
|
|
28
|
+
return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : `.${ctx.hostname}`;
|
|
29
|
+
}
|
|
30
|
+
function renderIdpComposeYaml() {
|
|
31
|
+
return [
|
|
32
|
+
'# Central IdP. See PLAN/deploy-runbook.md scenario E/F.',
|
|
33
|
+
'#',
|
|
34
|
+
'# ./run.sh',
|
|
35
|
+
'#',
|
|
36
|
+
'# Env split (matches `agenthippo deploy init`):',
|
|
37
|
+
'# ~/.agent-hippo/.env (+ optional ./.env) — hostname only',
|
|
38
|
+
'# ~/.agent-hippo/.env.auth (+ optional ./.env.auth) — JWT/OAuth; broker + oauth2-proxy ONLY',
|
|
39
|
+
'# Profiles: edge (SSO stack), tunnel (Cloudflare → Caddy)',
|
|
40
|
+
'# Org identity infra only — no agent runs here. Per-agent edges are generated separately',
|
|
41
|
+
'# by `agenthippo deploy init` and deployed on their own host/service.',
|
|
42
|
+
'services:',
|
|
43
|
+
' oauth2-proxy:',
|
|
44
|
+
' profiles: ["edge"]',
|
|
45
|
+
' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
|
|
46
|
+
' env_file:',
|
|
47
|
+
' - path: ${HOME}/.agent-hippo/.env',
|
|
48
|
+
' required: false',
|
|
49
|
+
' - path: .env',
|
|
50
|
+
' required: false',
|
|
51
|
+
' - path: ${HOME}/.agent-hippo/.env.auth',
|
|
52
|
+
' required: false',
|
|
53
|
+
' - path: .env.auth',
|
|
54
|
+
' required: false',
|
|
55
|
+
' environment:',
|
|
56
|
+
' OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180',
|
|
57
|
+
' OAUTH2_PROXY_UPSTREAMS: file:///dev/null',
|
|
58
|
+
' OAUTH2_PROXY_SET_XAUTHREQUEST: "true"',
|
|
59
|
+
' OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"',
|
|
60
|
+
' OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"',
|
|
61
|
+
' OAUTH2_PROXY_REVERSE_PROXY: "true"',
|
|
62
|
+
' expose: ["4180"]',
|
|
63
|
+
'',
|
|
64
|
+
' auth-broker:',
|
|
65
|
+
' profiles: ["edge"]',
|
|
66
|
+
' build:',
|
|
67
|
+
' context: .',
|
|
68
|
+
' dockerfile: Dockerfile.auth-broker',
|
|
69
|
+
' env_file:',
|
|
70
|
+
' - path: ${HOME}/.agent-hippo/.env',
|
|
71
|
+
' required: false',
|
|
72
|
+
' - path: .env',
|
|
73
|
+
' required: false',
|
|
74
|
+
' - path: ${HOME}/.agent-hippo/.env.auth',
|
|
75
|
+
' required: false',
|
|
76
|
+
' - path: .env.auth',
|
|
77
|
+
' required: false',
|
|
78
|
+
' environment:',
|
|
79
|
+
' AGENTHIPPO_AUTH_PORT: "9780"',
|
|
80
|
+
' AGENTHIPPO_AUTH_DATA_DIR: /data',
|
|
81
|
+
' # Proxy hops in front of the broker: 1 = Caddy only (edge profile), 2 = Cloudflare',
|
|
82
|
+
' # Tunnel → Caddy (run.sh sets 2; at 1 the rate limiter would key every tunnel',
|
|
83
|
+
' # client on cloudflared\'s IP — one shared bucket).',
|
|
84
|
+
' AGENTHIPPO_XFF_TRUSTED_HOPS: ${AGENTHIPPO_XFF_TRUSTED_HOPS:-1}',
|
|
85
|
+
' volumes: ["auth-broker-data:/data"]',
|
|
86
|
+
' expose: ["9780"]',
|
|
87
|
+
'',
|
|
88
|
+
' caddy:',
|
|
89
|
+
' profiles: ["edge"]',
|
|
90
|
+
' image: caddy:2.8-alpine',
|
|
91
|
+
' env_file:',
|
|
92
|
+
' - path: ${HOME}/.agent-hippo/.env',
|
|
93
|
+
' required: false',
|
|
94
|
+
' - path: .env',
|
|
95
|
+
' required: false',
|
|
96
|
+
' # With the tunnel profile, set AGENTHIPPO_CADDY_BIND=127.0.0.1 so a public-IP host',
|
|
97
|
+
' # cannot be hit directly on 80/443, bypassing Cloudflare.',
|
|
98
|
+
' # Bare-metal / BYO nginx or ALB already on 80/443? Set AGENTHIPPO_CADDY_HTTP_PORT /',
|
|
99
|
+
' # AGENTHIPPO_CADDY_HTTPS_PORT to alternate host ports and point your own reverse proxy',
|
|
100
|
+
' # at them — the container still listens on 80 internally, only the host mapping moves.',
|
|
101
|
+
' ports:',
|
|
102
|
+
' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTP_PORT:-80}:80"',
|
|
103
|
+
' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTPS_PORT:-443}:443"',
|
|
104
|
+
' volumes:',
|
|
105
|
+
' - ./Caddyfile:/etc/caddy/Caddyfile:ro',
|
|
106
|
+
' # Colocated agents (deploy init --colocate) drop a route snippet here — see Caddyfile.',
|
|
107
|
+
' - ./agents:/etc/caddy/agents:ro',
|
|
108
|
+
' networks: [default, colocate]',
|
|
109
|
+
' depends_on: [oauth2-proxy, auth-broker]',
|
|
110
|
+
'',
|
|
111
|
+
' cloudflared:',
|
|
112
|
+
' profiles: ["tunnel"]',
|
|
113
|
+
' image: cloudflare/cloudflared:2024.12.2',
|
|
114
|
+
' restart: unless-stopped',
|
|
115
|
+
' command: tunnel --config /etc/cloudflared/config.yml run',
|
|
116
|
+
' volumes:',
|
|
117
|
+
' - ./cloudflared.yml:/etc/cloudflared/config.yml:ro',
|
|
118
|
+
' # Set by run.sh from its argument (./run.sh /path/to/tunnel-credentials.json);',
|
|
119
|
+
' # falls back to the conventional location for a direct `docker compose` invocation.',
|
|
120
|
+
' - ${AGENTHIPPO_TUNNEL_CREDENTIALS:-${HOME}/.agent-hippo/tunnel-credentials.json}:/etc/cloudflared/credentials.json:ro',
|
|
121
|
+
' depends_on: [caddy]',
|
|
122
|
+
'',
|
|
123
|
+
'volumes:',
|
|
124
|
+
' auth-broker-data:',
|
|
125
|
+
'',
|
|
126
|
+
'networks:',
|
|
127
|
+
' default:',
|
|
128
|
+
' # Stable, explicitly-named network (not the implicit "idp_default") so a colocated agent\'s',
|
|
129
|
+
' # own compose project can join it reliably regardless of which directory `docker compose`',
|
|
130
|
+
' # runs from. Only Caddy needs it — its per-agent snippets `reverse_proxy` straight to the',
|
|
131
|
+
' # agent container by name; auth-broker is reached over the (already-shared) default network.',
|
|
132
|
+
' colocate:',
|
|
133
|
+
' name: agenthippo-idp-colocate',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n');
|
|
136
|
+
}
|
|
137
|
+
function renderIdpCaddyfile() {
|
|
138
|
+
const identityLines = (0, identity_1.renderIdentityLayer)({
|
|
139
|
+
mode: 'self-issued',
|
|
140
|
+
oauth2ProxyAddr: 'oauth2-proxy:4180',
|
|
141
|
+
authBrokerAddr: 'auth-broker:9780',
|
|
142
|
+
healthProxy: [' reverse_proxy auth-broker:9780'],
|
|
143
|
+
// No agent behind the IdP: a Bearer request to /v1/* doesn't exist here (404, not a
|
|
144
|
+
// silent pass-through), and an unauthenticated browser sees a plain confirmation once
|
|
145
|
+
// past the SSO gate.
|
|
146
|
+
agentProxy: [' respond "not_found" 404'],
|
|
147
|
+
catchallBody: [' respond "AgentHippo IdP" 200'],
|
|
148
|
+
});
|
|
149
|
+
return [
|
|
150
|
+
'# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
|
|
151
|
+
'# TLS terminates at Cloudflare; origin listens on HTTP only.',
|
|
152
|
+
'{',
|
|
153
|
+
' auto_https off',
|
|
154
|
+
'}',
|
|
155
|
+
'',
|
|
156
|
+
'# Same-host colocation: agents deployed with `deploy init --ingress edge-fed --colocate`',
|
|
157
|
+
'# on THIS host ship a route snippet instead of their own Caddy/oauth2-proxy/auth-broker.',
|
|
158
|
+
'# Drop it in ./agents/<name>.caddy (mounted in docker-compose.yml) and',
|
|
159
|
+
'# `docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile` to pick it up —',
|
|
160
|
+
'# no rebuild, no restart of the IdP itself.',
|
|
161
|
+
'import agents/*.caddy',
|
|
162
|
+
'',
|
|
163
|
+
`http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {`,
|
|
164
|
+
...identityLines,
|
|
165
|
+
'}',
|
|
166
|
+
'',
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
169
|
+
/** Render every artifact for `agenthippo idp init` (relative path → file content). Pure: no fs writes. */
|
|
170
|
+
function renderIdpArtifacts(ctx) {
|
|
171
|
+
const cookieDomain = resolveCookieDomain(ctx);
|
|
172
|
+
return {
|
|
173
|
+
'docker-compose.yml': renderIdpComposeYaml(),
|
|
174
|
+
'Dockerfile.auth-broker': (0, deploy_init_1.renderAuthBrokerDockerfile)(),
|
|
175
|
+
'Caddyfile': renderIdpCaddyfile(),
|
|
176
|
+
'.env.example': [
|
|
177
|
+
'# Caddy hostname for the central IdP. No agent runs on this stack — auth secrets:',
|
|
178
|
+
'# see .env.auth.example → ~/.agent-hippo/.env.auth (broker + oauth2-proxy only).',
|
|
179
|
+
'',
|
|
180
|
+
`AGENTHIPPO_PUBLIC_HOSTNAME=${ctx.hostname}`,
|
|
181
|
+
'',
|
|
182
|
+
'# Proxy hops / Caddy bind (optional; run.sh sets XFF hops=2 for tunnel)',
|
|
183
|
+
'# AGENTHIPPO_XFF_TRUSTED_HOPS=1',
|
|
184
|
+
'# AGENTHIPPO_CADDY_BIND=0.0.0.0',
|
|
185
|
+
'',
|
|
186
|
+
'# Bare metal / BYO nginx or ALB already on 80/443? Rebind Caddy to alternate host ports',
|
|
187
|
+
'# and point your reverse proxy at them (container still listens on 80 internally):',
|
|
188
|
+
'# AGENTHIPPO_CADDY_HTTP_PORT=8080',
|
|
189
|
+
'# AGENTHIPPO_CADDY_HTTPS_PORT=8443',
|
|
190
|
+
'',
|
|
191
|
+
].join('\n'),
|
|
192
|
+
'.env.auth.example': (0, deploy_init_1.renderAuthEnvExample)({ hostname: ctx.hostname, fed: false, cookieDomain }),
|
|
193
|
+
'cloudflared.yml.example': [
|
|
194
|
+
'# Copy to cloudflared.yml; the credentials JSON lives in ~/.agent-hippo/ (never in the repo).',
|
|
195
|
+
'# cloudflared tunnel create agenthippo-auth',
|
|
196
|
+
'# cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json',
|
|
197
|
+
`# cloudflared tunnel route dns agenthippo-auth ${ctx.hostname}`,
|
|
198
|
+
'tunnel: REPLACE_WITH_TUNNEL_ID',
|
|
199
|
+
'credentials-file: /etc/cloudflared/credentials.json',
|
|
200
|
+
'ingress:',
|
|
201
|
+
` - hostname: ${ctx.hostname}`,
|
|
202
|
+
' service: http://caddy:80',
|
|
203
|
+
' - service: http_status:404',
|
|
204
|
+
'',
|
|
205
|
+
].join('\n'),
|
|
206
|
+
'run.sh': [
|
|
207
|
+
'#!/usr/bin/env bash',
|
|
208
|
+
'# Start central IdP: Caddy + oauth2-proxy + auth-broker (+ Cloudflare tunnel, if requested).',
|
|
209
|
+
'# No agent here — per-agent edges are generated separately by `agenthippo deploy init`.',
|
|
210
|
+
'#',
|
|
211
|
+
'# Usage:',
|
|
212
|
+
'# ./run.sh bare metal / BYO nginx or ALB in front',
|
|
213
|
+
'# ./run.sh /path/to/tunnel-credentials.json Cloudflare Tunnel',
|
|
214
|
+
'#',
|
|
215
|
+
'# Bare metal / BYO nginx or ALB: run with no argument — starts --profile edge only.',
|
|
216
|
+
'# If 80/443 are already taken by your reverse proxy, set AGENTHIPPO_CADDY_HTTP_PORT/',
|
|
217
|
+
'# _HTTPS_PORT in .env first so Caddy rebinds elsewhere.',
|
|
218
|
+
'set -euo pipefail',
|
|
219
|
+
'cd "$(dirname "$0")"',
|
|
220
|
+
'if [[ $# -ge 1 ]]; then',
|
|
221
|
+
' export AGENTHIPPO_TUNNEL_CREDENTIALS="$1"',
|
|
222
|
+
' [[ -f "$AGENTHIPPO_TUNNEL_CREDENTIALS" ]] || {',
|
|
223
|
+
' echo "Tunnel credentials file not found: $AGENTHIPPO_TUNNEL_CREDENTIALS" >&2; exit 1; }',
|
|
224
|
+
' # Two proxy hops behind the tunnel (cloudflared → Caddy) — see docker-compose.yml.',
|
|
225
|
+
' export AGENTHIPPO_XFF_TRUSTED_HOPS="${AGENTHIPPO_XFF_TRUSTED_HOPS:-2}"',
|
|
226
|
+
' docker compose --profile edge --profile tunnel up -d --build',
|
|
227
|
+
` echo "IdP: https://\${AGENTHIPPO_PUBLIC_HOSTNAME:-${ctx.hostname}} (via Cloudflare Tunnel)"`,
|
|
228
|
+
'else',
|
|
229
|
+
' echo "No tunnel credentials path given — starting --profile edge only (no tunnel)."',
|
|
230
|
+
' echo "Front this host with your own nginx/ALB; see AGENTHIPPO_CADDY_HTTP_PORT/AGENTHIPPO_CADDY_BIND in .env."',
|
|
231
|
+
' echo "(To use a Cloudflare Tunnel instead: ./run.sh /path/to/tunnel-credentials.json)"',
|
|
232
|
+
' docker compose --profile edge up -d --build',
|
|
233
|
+
` echo "IdP: http://\${AGENTHIPPO_PUBLIC_HOSTNAME:-${ctx.hostname}} (behind your own reverse proxy)"`,
|
|
234
|
+
'fi',
|
|
235
|
+
'',
|
|
236
|
+
].join('\n'),
|
|
237
|
+
'agents/README.md': [
|
|
238
|
+
'# Colocated agent routes',
|
|
239
|
+
'',
|
|
240
|
+
'Drop `*.caddy` route snippets here for agents deployed on **this same host** via:',
|
|
241
|
+
'',
|
|
242
|
+
'```bash',
|
|
243
|
+
'agenthippo deploy init <pack> --target compose --ingress edge-fed --colocate',
|
|
244
|
+
'```',
|
|
245
|
+
'',
|
|
246
|
+
'That command generates `deploy/agent-route.caddy` (copy it here, renamed to something',
|
|
247
|
+
'identifying the agent) plus a `docker-compose.yml` containing only the `agent` service',
|
|
248
|
+
'(no Caddy/oauth2-proxy/auth-broker of its own — it joins this IdP\'s',
|
|
249
|
+
'`agenthippo-idp-colocate` network and rides the IdP\'s existing edge).',
|
|
250
|
+
'',
|
|
251
|
+
'After copying a new snippet in:',
|
|
252
|
+
'',
|
|
253
|
+
'```bash',
|
|
254
|
+
'docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile',
|
|
255
|
+
'```',
|
|
256
|
+
'',
|
|
257
|
+
'No rebuild, no restart of the IdP itself. See `../Caddyfile`\'s `import agents/*.caddy` and',
|
|
258
|
+
'`PLAN/deploy-runbook.md` scenario F for the full picture.',
|
|
259
|
+
'',
|
|
260
|
+
].join('\n'),
|
|
261
|
+
'README.md': [
|
|
262
|
+
'# Central IdP',
|
|
263
|
+
'',
|
|
264
|
+
'**One per organization.** Hosts Google SSO (oauth2-proxy) and the IDE auth-broker only —',
|
|
265
|
+
'no agent runs here. Agent edges are generated and deployed separately.',
|
|
266
|
+
'',
|
|
267
|
+
'Generated by `agenthippo idp init` — regenerate (don\'t hand-edit `Caddyfile`, which comes',
|
|
268
|
+
'straight from the same identity module every agent\'s Caddyfile uses) if you need to change',
|
|
269
|
+
'routing: that lives in the CLI itself, not in this output.',
|
|
270
|
+
'',
|
|
271
|
+
'This stack runs on **any host with Docker** — local laptop, team VM, cloud VM, or on-prem.',
|
|
272
|
+
'`./run.sh` picks the path below based on whether you pass it a tunnel credentials path.',
|
|
273
|
+
'',
|
|
274
|
+
'## Quick start',
|
|
275
|
+
'',
|
|
276
|
+
'```bash',
|
|
277
|
+
'cp .env.example ~/.agent-hippo/.env',
|
|
278
|
+
'cp .env.auth.example ~/.agent-hippo/.env.auth # fill in; never put AGENTHIPPO_AUTH_JWT_SECRET in .env',
|
|
279
|
+
'```',
|
|
280
|
+
'',
|
|
281
|
+
'**Cloudflare Tunnel** (no public ports to manage yourself):',
|
|
282
|
+
'',
|
|
283
|
+
'```bash',
|
|
284
|
+
'cp cloudflared.yml.example cloudflared.yml # set tunnel id',
|
|
285
|
+
'cloudflared tunnel create agenthippo-auth # writes ~/.cloudflared/<tunnel-id>.json',
|
|
286
|
+
'cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json',
|
|
287
|
+
'./run.sh ~/.agent-hippo/tunnel-credentials.json',
|
|
288
|
+
'```',
|
|
289
|
+
'',
|
|
290
|
+
'**Bare metal / BYO nginx or ALB already fronting this host:**',
|
|
291
|
+
'',
|
|
292
|
+
'```bash',
|
|
293
|
+
'# skip cloudflared.yml entirely. If 80/443 are already taken by your reverse proxy,',
|
|
294
|
+
'# set AGENTHIPPO_CADDY_HTTP_PORT/AGENTHIPPO_CADDY_HTTPS_PORT in .env first.',
|
|
295
|
+
'./run.sh # no argument — starts --profile edge only',
|
|
296
|
+
'# point your nginx/ALB at this host\'s Caddy (AGENTHIPPO_CADDY_BIND / _HTTP_PORT above)',
|
|
297
|
+
'```',
|
|
298
|
+
'',
|
|
299
|
+
'Agent edges (e.g. `cs.corp.example`) are separate — see `PLAN/deploy-runbook.md` scenarios E/F.',
|
|
300
|
+
'',
|
|
301
|
+
].join('\n'),
|
|
302
|
+
};
|
|
303
|
+
}
|
package/out/cli/index.js
CHANGED
|
@@ -77,6 +77,7 @@ const ask_1 = require("./commands/ask");
|
|
|
77
77
|
const serve_1 = require("./commands/serve");
|
|
78
78
|
const auth_broker_1 = require("./commands/auth-broker");
|
|
79
79
|
const deploy_1 = require("./commands/deploy");
|
|
80
|
+
const idp_1 = require("./commands/idp");
|
|
80
81
|
const pack_1 = require("./commands/pack");
|
|
81
82
|
const store_1 = require("./commands/store");
|
|
82
83
|
const anywhere_1 = require("./commands/anywhere");
|
|
@@ -247,22 +248,29 @@ program
|
|
|
247
248
|
// Deploy command - generate platform-native deploy artifacts
|
|
248
249
|
program
|
|
249
250
|
.command('deploy <action> [pack]')
|
|
250
|
-
.description('Generate a self-contained deploy staging dir (init). Targets: vm, compose, cloudrun, databricks-app, spcs, cloudflare, all')
|
|
251
|
+
.description('Generate a self-contained deploy staging dir (init). Targets: vm, compose, cloudrun, databricks-app, spcs, cloudflare, aws-ecs, azure-container-apps, all. Or pick a --blueprint instead of raw --target/--ingress.')
|
|
251
252
|
.option('-t, --target <target>', 'Deploy target', 'vm')
|
|
252
|
-
.option('
|
|
253
|
+
.option('--blueprint <id>', 'Named preset (sandbox, prospect-demo, team-pilot, regulated-air-gapped, enterprise-fleet) — resolves --target/--ingress for you (PLAN/deploy.md §3); overrides both when set')
|
|
254
|
+
.option('--platform <platform>', 'Which platform to use within --blueprint\'s supported set (defaults to the blueprint\'s first); ignored without --blueprint')
|
|
255
|
+
.option('-o, --out <path>', 'Staging directory (default: ./deploy-out/<pack>-<target> relative to cwd; run from the pack parent, not from inside the pack)')
|
|
253
256
|
.option('--packdir <path>', 'Pack directory (defaults to [pack] arg or cwd)')
|
|
254
257
|
.option('--image <ref>', 'Runtime image ref (default: engine runtime:latest, or :latest-cursor for cursor packs)')
|
|
255
258
|
.option('-p, --port <port>', 'Container port', '8080')
|
|
256
259
|
.option('--gateway <channels>', 'Agent Anywhere: bind channels (e.g. "slack") + add OpenClaw gateway service (compose target)')
|
|
257
260
|
.option('--hostname <host>', 'Public hostname for SSO edge (Caddy, OAuth, tunnel)', 'auth.agenthippo.ai')
|
|
258
|
-
.option('--ingress <mode>', 'Cloud Run
|
|
261
|
+
.option('--ingress <mode>', 'iap (Cloud Run built-in IAP only), edge (self-issued AgentHippo edge + Google OAuth — vm/compose/cloudrun/aws-ecs/azure-container-apps), or edge-fed (federated: auth via the central IdP, no per-target OAuth client — same targets as edge)', 'iap')
|
|
259
262
|
.option('--idp-url <url>', 'edge-fed: central IdP base URL browsers redirect to for login (e.g. https://auth.agenthippo.ai)', 'https://auth.agenthippo.ai')
|
|
260
263
|
.option('--cookie-domain <domain>', 'edge-fed: parent domain the shared auth cookie is scoped to (e.g. .agenthippo.ai)', '.agenthippo.ai')
|
|
264
|
+
.option('--colocate', 'edge-fed + vm/compose only: ride the org IdP\'s already-running Caddy + auth-broker instead of shipping this agent\'s own (PLAN/deploy.md §4 state 4)')
|
|
265
|
+
.option('--model-routing <mode>', 'Cloud Run + edge-fed: sidecar (in-pod LiteLLM+OTEL, default) or gateway (agent → gateway.agenthippo.ai directly, lean)')
|
|
261
266
|
.option('--force', 'Overwrite existing files')
|
|
262
267
|
.action(async (action, pack, options) => {
|
|
263
268
|
const ingress = options.ingress === 'edge' ? 'edge' : options.ingress === 'edge-fed' ? 'edge-fed' : 'iap';
|
|
269
|
+
const modelRouting = options.modelRouting === 'gateway' ? 'gateway' : options.modelRouting === 'sidecar' ? 'sidecar' : undefined;
|
|
264
270
|
await (0, deploy_1.deployCommand)(action, pack, {
|
|
265
271
|
target: options.target,
|
|
272
|
+
blueprint: options.blueprint,
|
|
273
|
+
platform: options.platform,
|
|
266
274
|
out: options.out,
|
|
267
275
|
image: options.image,
|
|
268
276
|
gateway: options.gateway,
|
|
@@ -273,6 +281,24 @@ program
|
|
|
273
281
|
ingress,
|
|
274
282
|
idpUrl: options.idpUrl,
|
|
275
283
|
cookieDomain: options.cookieDomain,
|
|
284
|
+
colocate: Boolean(options.colocate),
|
|
285
|
+
modelRouting,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
// IdP command - generate the central IdP stack (org identity infra, set up once)
|
|
289
|
+
program
|
|
290
|
+
.command('idp <action>')
|
|
291
|
+
.description('Generate the central IdP stack (init) — Caddy + oauth2-proxy + auth-broker. One per organization; agents federate against it with `deploy init --ingress edge-fed`.')
|
|
292
|
+
.option('--hostname <host>', 'Public hostname the IdP is reachable at (e.g. auth.corp.example) — required')
|
|
293
|
+
.option('--cookie-domain <domain>', 'Parent domain the shared auth cookie is scoped to (e.g. .corp.example); defaults to the hostname\'s own domain')
|
|
294
|
+
.option('-o, --out <path>', 'Output directory (default: ./enterprise-deploy/idp)')
|
|
295
|
+
.option('--force', 'Overwrite existing files')
|
|
296
|
+
.action(async (action, options) => {
|
|
297
|
+
await (0, idp_1.idpCommand)(action, {
|
|
298
|
+
hostname: options.hostname,
|
|
299
|
+
cookieDomain: options.cookieDomain,
|
|
300
|
+
out: options.out,
|
|
301
|
+
force: Boolean(options.force),
|
|
276
302
|
});
|
|
277
303
|
});
|
|
278
304
|
// Pack command - pack management
|
|
@@ -326,8 +352,12 @@ const gatewayCmd = program.command('gateway').description('Gateway outbound mess
|
|
|
326
352
|
gatewayCmd
|
|
327
353
|
.command('send')
|
|
328
354
|
.description('Send a proactive message via the gateway (WebSocket RPC; hippoclaw CLI fallback)')
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
// --outbound/--agent are optional: under `serve` with the egress broker active
|
|
356
|
+
// (AGENTHIPPO_EGRESS_URL + AGENTHIPPO_TURN_NONCE set), the broker resolves the destination
|
|
357
|
+
// server-side from the turn nonce and neither is needed (secure-obo-design §7.6). Off-serve
|
|
358
|
+
// the direct path still requires both (validated in gatewaySendCommand).
|
|
359
|
+
.option('--outbound <path>', 'Session gateway-outbound.json (see AGENTHIPPO_GATEWAY_OUTBOUND_FILE)')
|
|
360
|
+
.option('-a, --agent <id>', 'Agent pack id')
|
|
331
361
|
.requiredOption('-m, --message <text>', 'Message to send')
|
|
332
362
|
.option('--media <path-or-url>', 'Attach media (image/audio/video/document); local path or URL. Sent via gateway RPC so the bot token resolves from the gateway runtime snapshot.')
|
|
333
363
|
.option('-t, --target <target>', 'Explicit target (e.g. channel:C01234567); defaults to last inbound route')
|
|
@@ -336,8 +366,8 @@ gatewayCmd
|
|
|
336
366
|
.option('--json', 'Output handle + result as JSON')
|
|
337
367
|
.action(async (options) => {
|
|
338
368
|
await (0, gateway_1.gatewaySendCommand)({
|
|
339
|
-
outbound: options.outbound,
|
|
340
|
-
agent: options.agent,
|
|
369
|
+
outbound: options.outbound ?? '',
|
|
370
|
+
agent: options.agent ?? '',
|
|
341
371
|
message: options.message,
|
|
342
372
|
media: options.media,
|
|
343
373
|
target: options.target,
|
package/out/cli/pack-loader.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.loadAgentPack = loadAgentPack;
|
|
|
42
42
|
exports.getDefaultConfig = getDefaultConfig;
|
|
43
43
|
exports.getEnvDefaultModel = getEnvDefaultModel;
|
|
44
44
|
exports.getApiConfig = getApiConfig;
|
|
45
|
+
exports.hasCredentialForEngine = hasCredentialForEngine;
|
|
45
46
|
exports.getFileAccess = getFileAccess;
|
|
46
47
|
/**
|
|
47
48
|
* Agent Pack Loader
|
|
@@ -330,6 +331,14 @@ function validateManifest(manifest, sourcePath) {
|
|
|
330
331
|
if (manifest.spec?.cheapModel !== undefined && typeof manifest.spec.cheapModel !== 'string') {
|
|
331
332
|
errors.push('Invalid spec.cheapModel: must be a string when provided');
|
|
332
333
|
}
|
|
334
|
+
if (manifest.spec?.dataPlane !== undefined) {
|
|
335
|
+
if (typeof manifest.spec.dataPlane !== 'object' || !manifest.spec.dataPlane.connector?.trim()) {
|
|
336
|
+
errors.push('Invalid spec.dataPlane: requires a `connector` path (stdio MCP module run broker-side)');
|
|
337
|
+
}
|
|
338
|
+
if (manifest.spec.dataPlane?.provider !== undefined && typeof manifest.spec.dataPlane.provider !== 'string') {
|
|
339
|
+
errors.push('Invalid spec.dataPlane.provider: must be a string when provided');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
333
342
|
if (errors.length > 0) {
|
|
334
343
|
throw new Error(`Invalid agent manifest at ${sourcePath}:\n - ${errors.join('\n - ')}`);
|
|
335
344
|
}
|
|
@@ -354,20 +363,46 @@ function getEnvDefaultModel() {
|
|
|
354
363
|
}
|
|
355
364
|
/**
|
|
356
365
|
* Get API configuration from environment.
|
|
366
|
+
*
|
|
367
|
+
* Returns the credentials keyed by destination (litellm / openai / anthropic) rather than a
|
|
368
|
+
* single collapsed key. The engine selects the one matching the endpoint it dials — see
|
|
369
|
+
* resolveApiRouting — so a codex deployment can no longer send an Anthropic key to OpenAI.
|
|
357
370
|
*/
|
|
358
371
|
function getApiConfig() {
|
|
359
372
|
const litellmApiKey = getEnvVar('AGENTHIPPO_LITELLM_API_KEY', 'AGENTIDE_LITELLM_API_KEY');
|
|
360
373
|
const litellmBaseUrl = getEnvVar('AGENTHIPPO_LITELLM_BASE_URL', 'AGENTIDE_LITELLM_BASE_URL');
|
|
361
374
|
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY;
|
|
362
375
|
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
363
|
-
const apiKey = litellmApiKey || anthropicApiKey || openaiApiKey;
|
|
364
376
|
const useLiteLLM = Boolean(litellmApiKey && litellmBaseUrl);
|
|
365
377
|
return {
|
|
366
|
-
|
|
378
|
+
// Kept only for the "is any credential present at all?" boot check; routing never uses it.
|
|
379
|
+
apiKey: litellmApiKey || anthropicApiKey || openaiApiKey,
|
|
367
380
|
baseUrl: litellmBaseUrl,
|
|
368
381
|
useLiteLLM,
|
|
382
|
+
litellmApiKey,
|
|
383
|
+
openaiApiKey,
|
|
384
|
+
anthropicApiKey,
|
|
369
385
|
};
|
|
370
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Does the environment carry a usable credential for `engine`? Engine-aware so a codex
|
|
389
|
+
* deployment with only an Anthropic key (or vice-versa) fails loudly at boot instead of
|
|
390
|
+
* 401-ing at the first model call. A configured LiteLLM proxy satisfies every engine.
|
|
391
|
+
*/
|
|
392
|
+
function hasCredentialForEngine(engine, cfg = getApiConfig()) {
|
|
393
|
+
if (cfg.useLiteLLM) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
const e = engine.trim().toLowerCase();
|
|
397
|
+
if (e === 'claude') {
|
|
398
|
+
return Boolean(cfg.anthropicApiKey);
|
|
399
|
+
}
|
|
400
|
+
if (e === 'codex') {
|
|
401
|
+
return Boolean(cfg.openaiApiKey);
|
|
402
|
+
}
|
|
403
|
+
// Custom engines can authenticate via their own manifest-declared env var — don't gate them here.
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
371
406
|
/**
|
|
372
407
|
* Get file access mode from pack manifest.
|
|
373
408
|
*/
|
package/out/cli/runtime.js
CHANGED
|
@@ -315,9 +315,16 @@ function buildCliEnvPatch(args) {
|
|
|
315
315
|
chatSessionId: args.chatSessionId,
|
|
316
316
|
}),
|
|
317
317
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// When serve runs the egress broker (secure-obo-design §7.6), the gateway token must NOT
|
|
319
|
+
// reach the agent's trust domain: the broker holds it, the agent presents only the turn
|
|
320
|
+
// nonce. AGENTHIPPO_EGRESS_URL in the process env is that signal — with it set, never bake
|
|
321
|
+
// the token into a child env (this closes §7.4 gap 1 for custom engines; codex/claude are
|
|
322
|
+
// covered by serve stripping the token from its own process.env).
|
|
323
|
+
if (!process.env.AGENTHIPPO_EGRESS_URL?.trim()) {
|
|
324
|
+
const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
|
|
325
|
+
if (gatewayToken) {
|
|
326
|
+
env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
|
|
327
|
+
}
|
|
321
328
|
}
|
|
322
329
|
// Claude SDK refuses nested sessions when CLAUDECODE is present.
|
|
323
330
|
// Force-unset in child runtime for CLI ask/chat/serve subprocesses.
|
|
@@ -36,13 +36,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.TurnGate = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.IDENTITY_HEADER_ENV = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
|
|
39
|
+
exports.TurnGate = exports.TURN_LIMIT_BUSY_MESSAGE = exports.RATE_LIMIT_BUSY_MESSAGE = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.SESSION_WITHOUT_USER_REASON = exports.IDENTITY_HEADER_ENV = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
|
|
40
40
|
exports.resolveStateDir = resolveStateDir;
|
|
41
|
+
exports.resolveAnalyticsSessionsDir = resolveAnalyticsSessionsDir;
|
|
41
42
|
exports.resolveBindHost = resolveBindHost;
|
|
42
43
|
exports.clientIp = clientIp;
|
|
43
44
|
exports.resolveUserId = resolveUserId;
|
|
44
45
|
exports.resolveDataToken = resolveDataToken;
|
|
45
46
|
exports.peerToUserId = peerToUserId;
|
|
47
|
+
exports.userNamespaceHash = userNamespaceHash;
|
|
48
|
+
exports.resolveServeSessionKey = resolveServeSessionKey;
|
|
46
49
|
exports.corsHeaders = corsHeaders;
|
|
47
50
|
exports.createTurnGateFromEnv = createTurnGateFromEnv;
|
|
48
51
|
exports.createRejectionWarner = createRejectionWarner;
|
|
@@ -60,6 +63,8 @@ exports.readBodyWithCap = readBodyWithCap;
|
|
|
60
63
|
*/
|
|
61
64
|
const os = __importStar(require("os"));
|
|
62
65
|
const path = __importStar(require("path"));
|
|
66
|
+
const crypto = __importStar(require("crypto"));
|
|
67
|
+
const paths_1 = require("../utils/paths");
|
|
63
68
|
/** Default cap on a single request body (1 MiB). Chat messages are small; large bodies are abuse or bugs. */
|
|
64
69
|
exports.DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
65
70
|
/**
|
|
@@ -82,6 +87,26 @@ function resolveStateDir(stateDirOpt, env = process.env) {
|
|
|
82
87
|
}
|
|
83
88
|
return { dir: path.join(os.tmpdir(), 'agenthippo-server'), ephemeral: true };
|
|
84
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Where serve's Layer 2 analytics (tool-call/warrant JSONL) should live. Precedence:
|
|
92
|
+
* 1. AGENT_HOME_DIR / AGENTHIPPO_HOME set → getAgentideHome()/analytics/sessions — the
|
|
93
|
+
* exact location sessionJsonlFleet.ts and query-analytics.sh already scan by default,
|
|
94
|
+
* so this data stays discoverable to Spotlight/DuckDB.
|
|
95
|
+
* 2. Neither set, but the resolved state dir is non-ephemeral (an explicit --state-dir
|
|
96
|
+
* was given) → <stateDir>/analytics/sessions. Not the canonical scan location, but
|
|
97
|
+
* still durable — never regress below whatever durability the caller already
|
|
98
|
+
* established just to be discoverable.
|
|
99
|
+
* 3. Neither set and state is ephemeral (no --state-dir, no home-dir env var at all) →
|
|
100
|
+
* falls through to getAgentideHome()'s own os.homedir() default, matching the
|
|
101
|
+
* already-correct IDE-local behavior — nothing durable was configured either way.
|
|
102
|
+
*/
|
|
103
|
+
function resolveAnalyticsSessionsDir(stateDir, stateDirEphemeral, env = process.env) {
|
|
104
|
+
const homeOverride = env.AGENT_HOME_DIR?.trim() || env.AGENTHIPPO_HOME?.trim();
|
|
105
|
+
if (!homeOverride && !stateDirEphemeral) {
|
|
106
|
+
return path.join(stateDir, 'analytics', 'sessions');
|
|
107
|
+
}
|
|
108
|
+
return path.join((0, paths_1.getAgentideHome)(), 'analytics', 'sessions');
|
|
109
|
+
}
|
|
85
110
|
/** Loopback default — safe everywhere a human runs `serve` locally. */
|
|
86
111
|
exports.DEFAULT_BIND_HOST = '127.0.0.1';
|
|
87
112
|
/** Env var for the listen address (used by generated deploy Dockerfiles). */
|
|
@@ -275,6 +300,36 @@ function peerToUserId(peer) {
|
|
|
275
300
|
const channel = peer.channel?.trim();
|
|
276
301
|
return channel ? `${channel}:${id}` : id;
|
|
277
302
|
}
|
|
303
|
+
/** Stable reason code when a session_id is presented without a verified user on a pinned-identity deployment. */
|
|
304
|
+
exports.SESSION_WITHOUT_USER_REASON = 'session_without_verified_user';
|
|
305
|
+
/** Filesystem/S3-safe per-user namespace segment (user ids are emails/subjects — never raw path segments). */
|
|
306
|
+
function userNamespaceHash(userId) {
|
|
307
|
+
return crypto.createHash('sha256').update(userId).digest('hex').slice(0, 16);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Namespace the session key by the verified user (secure-obo-design §3.4 / §8 Phase 0).
|
|
311
|
+
*
|
|
312
|
+
* `session_id` is client-supplied; un-namespaced, any authenticated user who guesses another
|
|
313
|
+
* user's session_id attaches to that session's conversation state (which holds the other
|
|
314
|
+
* user's row-filtered query results). Prefixing the key with a hash of the verified user makes
|
|
315
|
+
* cross-user attachment structurally impossible without changing client behavior.
|
|
316
|
+
*
|
|
317
|
+
* Fail-closed rule: when the deployment pins an identity header (it declared a real
|
|
318
|
+
* proxy-verified identity exists) and a request carries a session_id but no verified user,
|
|
319
|
+
* reject — never let an unauthenticated caller name a session. Unpinned deployments
|
|
320
|
+
* (local dev, no identity boundary at all) keep today's raw-key behavior.
|
|
321
|
+
*/
|
|
322
|
+
function resolveServeSessionKey(sessionId, userId, env = process.env) {
|
|
323
|
+
const sid = sessionId?.trim();
|
|
324
|
+
const uid = userId?.trim();
|
|
325
|
+
if (uid) {
|
|
326
|
+
return { sessionKey: `u${userNamespaceHash(uid)}--${sid || 'default'}` };
|
|
327
|
+
}
|
|
328
|
+
if (sid && env[exports.IDENTITY_HEADER_ENV]?.trim()) {
|
|
329
|
+
return { sessionKey: '', rejected: exports.SESSION_WITHOUT_USER_REASON };
|
|
330
|
+
}
|
|
331
|
+
return { sessionKey: sid || 'anon-unknown' };
|
|
332
|
+
}
|
|
278
333
|
/** Build CORS headers. Serve is always behind a proxy; CORS is wide-open here. */
|
|
279
334
|
function corsHeaders() {
|
|
280
335
|
return {
|
|
@@ -320,6 +375,10 @@ exports.TURN_QUEUE_ENV = 'AGENTHIPPO_TURN_QUEUE';
|
|
|
320
375
|
exports.TURN_GATE_RETRY_AFTER_SECONDS = 15;
|
|
321
376
|
/** User-visible capacity message for gateway channels (Slack/Telegram can't receive HTTP 429). */
|
|
322
377
|
exports.TURN_GATE_BUSY_MESSAGE = 'At capacity — please retry in a moment';
|
|
378
|
+
/** User-visible message for gateway channels when the per-identity RateLimiter rejects. */
|
|
379
|
+
exports.RATE_LIMIT_BUSY_MESSAGE = 'Rate limit exceeded — please retry in a moment';
|
|
380
|
+
/** User-visible message for gateway channels when the per-session TurnLimiter rejects. */
|
|
381
|
+
exports.TURN_LIMIT_BUSY_MESSAGE = 'Per-session turn limit reached';
|
|
323
382
|
/**
|
|
324
383
|
* Process-wide concurrency gate for engine turns (semaphore + bounded FIFO).
|
|
325
384
|
*
|
package/out/cli/state-sync.js
CHANGED
|
@@ -216,17 +216,20 @@ async function pathExists(p) {
|
|
|
216
216
|
return false;
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
-
async function copyDirIfExists(src, dest) {
|
|
220
|
-
if (!(await pathExists(src))) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
await fsp.mkdir(dest, { recursive: true });
|
|
224
|
-
await fsp.cp(src, dest, { recursive: true, force: true });
|
|
225
|
-
}
|
|
226
219
|
/**
|
|
227
|
-
* Build a local staging tree for session S:
|
|
228
|
-
*
|
|
229
|
-
*
|
|
220
|
+
* Build a local staging tree for session S: provider-sessions/<shards for S> only.
|
|
221
|
+
*
|
|
222
|
+
* `<stateDir>/agents/<agentId>/{claude,codex}` (the SDK's own on-disk session/config
|
|
223
|
+
* cache — see agentpack/paths.ts) is deliberately NOT included here. That directory is
|
|
224
|
+
* shared by every session/user of the same pack on this replica, keyed only by agentId —
|
|
225
|
+
* never session-scoped. Treating it as part of one session's push/restore let one
|
|
226
|
+
* session's stale snapshot force-overwrite the live SDK cache for every other
|
|
227
|
+
* concurrently active session on the replica (fs.cp force:true in the old
|
|
228
|
+
* mergeSessionStagingIntoState). It's local-disk-only now, same as before object-store
|
|
229
|
+
* sync existed: it lives and dies with its replica. A resume target missing after a
|
|
230
|
+
* replica change already falls back to a fresh SDK session — the same path every
|
|
231
|
+
* session's true first turn on any replica already takes — so this doesn't introduce a
|
|
232
|
+
* new failure mode, it just stops the wrong-session-data clobber.
|
|
230
233
|
*/
|
|
231
234
|
async function stageSessionPushDir(stateDir, sessionId, stagingDir) {
|
|
232
235
|
await fsp.rm(stagingDir, { recursive: true, force: true });
|
|
@@ -239,11 +242,10 @@ async function stageSessionPushDir(stateDir, sessionId, stagingDir) {
|
|
|
239
242
|
await fsp.copyFile(path.join(stateDir, 'provider-sessions', name), path.join(psDest, name));
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
|
-
|
|
243
|
-
const hasAgents = await pathExists(path.join(stagingDir, 'agents'));
|
|
244
|
-
return shards.length > 0 || hasAgents;
|
|
245
|
+
return shards.length > 0;
|
|
245
246
|
}
|
|
246
|
-
/** Merge a restored staging tree back into stateDir
|
|
247
|
+
/** Merge a restored staging tree back into stateDir (provider-session shards only — see
|
|
248
|
+
* stageSessionPushDir for why the SDK config cache is intentionally excluded). */
|
|
247
249
|
async function mergeSessionStagingIntoState(stagingDir, stateDir) {
|
|
248
250
|
const psSrc = path.join(stagingDir, 'provider-sessions');
|
|
249
251
|
if (await pathExists(psSrc)) {
|
|
@@ -256,7 +258,6 @@ async function mergeSessionStagingIntoState(stagingDir, stateDir) {
|
|
|
256
258
|
await fsp.copyFile(path.join(psSrc, name), path.join(psDest, name));
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
|
-
await copyDirIfExists(path.join(stagingDir, 'agents'), path.join(stateDir, 'agents'));
|
|
260
261
|
}
|
|
261
262
|
/** Download state from the bucket into stateDir on boot. An empty bucket is a no-op. */
|
|
262
263
|
async function restoreState(cfg, stateDir, log) {
|