@guuey/config 0.1.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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/agent-loader.d.ts +65 -0
- package/dist/agent-loader.d.ts.map +1 -0
- package/dist/agent-loader.js +155 -0
- package/dist/agent.d.ts +284 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +426 -0
- package/dist/app.d.ts +26 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +54 -0
- package/dist/ggui.d.ts +36 -0
- package/dist/ggui.d.ts.map +1 -0
- package/dist/ggui.js +51 -0
- package/dist/hosting.d.ts +78 -0
- package/dist/hosting.d.ts.map +1 -0
- package/dist/hosting.js +30 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/loader.d.ts +70 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +144 -0
- package/dist/mcp-proxy.d.ts +123 -0
- package/dist/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp-proxy.js +133 -0
- package/dist/mcp-servers.d.ts +173 -0
- package/dist/mcp-servers.d.ts.map +1 -0
- package/dist/mcp-servers.js +147 -0
- package/dist/registry.d.ts +35 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +80 -0
- package/dist/schema.d.ts +204 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +125 -0
- package/dist/system-prompt.d.ts +13 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +19 -0
- package/package.json +56 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guuey.json#agent` — the agent section.
|
|
3
|
+
*
|
|
4
|
+
* The agent section describes the deployable agent: framework + model
|
|
5
|
+
* + system prompt + MCP host config + platform-feature opt-ins + deploy
|
|
6
|
+
* config. Read by `@guuey/host` at pod boot to construct the framework
|
|
7
|
+
* adapter; read by `@guuey/cli` to validate before submitting a deploy.
|
|
8
|
+
*
|
|
9
|
+
* Lives inside `guuey.json` post-2026-05-25 platform-architecture merge
|
|
10
|
+
* (slice 7.2). Previously a separate `agent.json` file. See
|
|
11
|
+
* `docs/plans/2026-05-25-platform-architecture.md` §14.2 for the
|
|
12
|
+
* field-by-field migration.
|
|
13
|
+
*
|
|
14
|
+
* **Minimal valid section** (all other fields default):
|
|
15
|
+
*
|
|
16
|
+
* ```jsonc
|
|
17
|
+
* {
|
|
18
|
+
* "framework": "claude-agent-sdk",
|
|
19
|
+
* "model": "claude-sonnet-5",
|
|
20
|
+
* "systemPrompt": { "file": "prompts/system.md" }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Defaults applied by the pod runtime when fields are absent:
|
|
25
|
+
* - `framework` → `'claude-agent-sdk'`
|
|
26
|
+
* - `mcpServers` → `{ ggui: { url: 'https://mcp.ggui.ai' } }` (platform default; declaring `mcpServers` REPLACES this — not merged)
|
|
27
|
+
* - `model` → framework-chosen default (Claude SDK → `claude-sonnet-5`)
|
|
28
|
+
* - `systemPrompt`→ `GUUEY_DEFAULT_SYSTEM_PROMPT` from `./system-prompt`
|
|
29
|
+
* - `auth` → `'anonymous'`
|
|
30
|
+
* - `memory` → `'thread'`
|
|
31
|
+
* - `storage` → `['user', 'app']`
|
|
32
|
+
* - `endpoint` → `{ kind: 'invoke', streaming: true }`
|
|
33
|
+
* - `deploy` → `{ size: 'xs', region: 'us-east-1' }`
|
|
34
|
+
*
|
|
35
|
+
* **Rules for extending:**
|
|
36
|
+
*
|
|
37
|
+
* 1. **Additive only within `schema: '1'` (top-level).** New optional fields on existing
|
|
38
|
+
* objects are safe. Breaking changes bump the file-level `schema` to `'2'`.
|
|
39
|
+
* 2. **Framework-neutral by default.** Fields meaningful to only one adapter (e.g. Claude's
|
|
40
|
+
* `permissions`, OpenAI's `tools.functions`) belong on a `framework`-scoped sub-block.
|
|
41
|
+
*/
|
|
42
|
+
import { z } from 'zod';
|
|
43
|
+
import { AGENT_SIZES } from './hosting.js';
|
|
44
|
+
/**
|
|
45
|
+
* Supported framework adapters. The pod runtime selects the matching
|
|
46
|
+
* `@guuey/framework-*` adapter at boot. `vanilla` skips the framework
|
|
47
|
+
* layer entirely — the agent loop is the bare Anthropic Messages API
|
|
48
|
+
* call with manual MCP tool wiring. Useful for benchmarking and for
|
|
49
|
+
* adapters not yet built.
|
|
50
|
+
*/
|
|
51
|
+
export const AGENT_FRAMEWORKS = [
|
|
52
|
+
'claude-agent-sdk',
|
|
53
|
+
'openai-agents-sdk',
|
|
54
|
+
'google-adk',
|
|
55
|
+
'vanilla',
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Static header map — forwarded on every request. Values may use `${env.NAME}`
|
|
59
|
+
* placeholders; the pod's env-substitution pass fills them at call time.
|
|
60
|
+
* Secrets MUST be referenced via `${env.NAME}` and declared in `agent.secrets`.
|
|
61
|
+
*/
|
|
62
|
+
const HeadersSchema = z.record(z.string().min(1), z.string());
|
|
63
|
+
/**
|
|
64
|
+
* `kind: 'colocated'` — MCP server runs as a stdio child **inside the agent
|
|
65
|
+
* pod** (co-locate = same gVisor sandbox). COGS: ~$0 (rides the agent pod).
|
|
66
|
+
* Nocode runtime spawns `command [args…]` at boot.
|
|
67
|
+
*/
|
|
68
|
+
const ColocatedMcp = z.strictObject({
|
|
69
|
+
kind: z.literal('colocated'),
|
|
70
|
+
/** Executable name or path. Required. */
|
|
71
|
+
command: z.string().min(1),
|
|
72
|
+
/** Argv beyond `command`. */
|
|
73
|
+
args: z.array(z.string()).optional(),
|
|
74
|
+
/** Static headers (less common for stdio, but supported for symmetry). */
|
|
75
|
+
headers: HeadersSchema.optional(),
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* `kind: 'hosted'` — a workspace-owned registry MCP running on guuey's
|
|
79
|
+
* `mcp-servers.guuey.com` fleet (Starter+). At least one of `server` or `source`
|
|
80
|
+
* must be set:
|
|
81
|
+
*
|
|
82
|
+
* - `server: '<id>'` — reuse an existing registry MCP by id.
|
|
83
|
+
* - `source: './path'` — build-or-reuse by workspace-unique name; the
|
|
84
|
+
* deploy-controller resolves to a `server` id and writes it back — WITHOUT
|
|
85
|
+
* removing `source`, so both are legitimately present after a `guuey deploy`
|
|
86
|
+
* (`server` wins at resolve time; `source` remains the build recipe).
|
|
87
|
+
*/
|
|
88
|
+
const HostedMcp = z
|
|
89
|
+
.strictObject({
|
|
90
|
+
kind: z.literal('hosted'),
|
|
91
|
+
/** Existing registry MCP id. May coexist with `source` post-deploy write-back. */
|
|
92
|
+
server: z.string().min(1).optional(),
|
|
93
|
+
/** Source directory relative to `guuey.json`. May coexist with `server` post-deploy write-back. */
|
|
94
|
+
source: z.string().min(1).optional(),
|
|
95
|
+
/** Local dev-loop port (`guuey dev`) this MCP is served on for name→localhost URL resolution. */
|
|
96
|
+
devPort: z.number().int().min(1).max(65535).optional(),
|
|
97
|
+
})
|
|
98
|
+
.refine((v) => v.server != null || v.source != null, {
|
|
99
|
+
message: 'hosted MCP needs `server`and/or`source`',
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* `kind: 'proxied'` — a 3rd-party SaaS MCP reached through the mcp-proxy
|
|
103
|
+
* credential broker (Case C). Schema is present now; runtime support lands v2.
|
|
104
|
+
*/
|
|
105
|
+
const ProxiedMcp = z.strictObject({
|
|
106
|
+
kind: z.literal('proxied'),
|
|
107
|
+
/** mcp-proxy connection id (from `guuey connections add`). */
|
|
108
|
+
connection: z.string().min(1),
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* `kind: 'external'` — builder-hosted MCP at an arbitrary URL.
|
|
112
|
+
*
|
|
113
|
+
* - `transport` defaults to `'http'` (StreamableHTTP).
|
|
114
|
+
* - `federate: true` makes guuey mint a per-invoke JWT with `aud = url` that
|
|
115
|
+
* the builder's MCP validates against the guuey JWKS. Omit for plain URL +
|
|
116
|
+
* optional static `headers`.
|
|
117
|
+
*/
|
|
118
|
+
const ExternalMcp = z.strictObject({
|
|
119
|
+
kind: z.literal('external'),
|
|
120
|
+
/** Full HTTP/SSE base URL. */
|
|
121
|
+
url: z.url(),
|
|
122
|
+
/** Transport protocol. Defaults to `'http'` (StreamableHTTP). */
|
|
123
|
+
transport: z.enum(['http', 'sse']).optional(),
|
|
124
|
+
/**
|
|
125
|
+
* Mint a per-invoke `aud = url` JWT and inject it as `Authorization: Bearer`.
|
|
126
|
+
* The builder's MCP verifies it against the guuey JWKS (T6b).
|
|
127
|
+
*/
|
|
128
|
+
federate: z.boolean().optional(),
|
|
129
|
+
/** Static headers forwarded on every request. Values may use `${env.NAME}` placeholders. */
|
|
130
|
+
headers: HeadersSchema.optional(),
|
|
131
|
+
/** Local dev-loop port (`guuey dev`) this MCP is served on for name→localhost URL resolution. */
|
|
132
|
+
devPort: z.number().int().min(1).max(65535).optional(),
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* A single MCP server entry inside `agent.mcpServers`.
|
|
136
|
+
*
|
|
137
|
+
* Discriminated union on `kind` — one slot per hosting mode:
|
|
138
|
+
* - `colocated` — stdio child inside the agent pod
|
|
139
|
+
* - `hosted` — guuey-hosted registry MCP (Starter+)
|
|
140
|
+
* - `proxied` — 3rd-party SaaS via mcp-proxy credential broker (v2)
|
|
141
|
+
* - `external` — builder-hosted, reached by URL (plain or federated)
|
|
142
|
+
*/
|
|
143
|
+
const McpServerSchema = z.discriminatedUnion('kind', [
|
|
144
|
+
ColocatedMcp,
|
|
145
|
+
HostedMcp,
|
|
146
|
+
ProxiedMcp,
|
|
147
|
+
ExternalMcp,
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* Tool-gate block — allowlist applied first (intersect with what the MCP
|
|
151
|
+
* server advertises), then denylist subtracts. Tool names are MCP-namespaced
|
|
152
|
+
* (`"<server>.<tool>"`). Bare names match across all connected servers.
|
|
153
|
+
*/
|
|
154
|
+
const ToolGatesSchema = z.strictObject({
|
|
155
|
+
allowlist: z.array(z.string().min(1)).optional(),
|
|
156
|
+
denylist: z.array(z.string().min(1)).optional(),
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* Runtime knobs the pod applies when constructing the framework adapter.
|
|
160
|
+
* All optional with framework-chosen defaults.
|
|
161
|
+
*
|
|
162
|
+
* - `maxTurns` — cap on agent loop turns per user message. Stops runaway
|
|
163
|
+
* loops on misbehaving prompts. Default: framework default
|
|
164
|
+
* (Claude SDK = 25).
|
|
165
|
+
* - `temperature` — model sampling temperature passthrough.
|
|
166
|
+
*/
|
|
167
|
+
const RuntimeConfigSchema = z.strictObject({
|
|
168
|
+
maxTurns: z.number().int().min(1).max(200).optional(),
|
|
169
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Claude Agent SDK-specific knobs. Lives on a `framework` discriminator so
|
|
173
|
+
* other adapters don't accidentally read fields they don't understand.
|
|
174
|
+
*/
|
|
175
|
+
const ClaudePermissionsSchema = z.strictObject({
|
|
176
|
+
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions']).optional(),
|
|
177
|
+
});
|
|
178
|
+
const ClaudeFrameworkConfigSchema = z.strictObject({
|
|
179
|
+
permissions: ClaudePermissionsSchema.optional(),
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* System prompt — string inline OR `{ file: 'prompts/system.md' }`.
|
|
183
|
+
* File references are resolved relative to `guuey.json` by the loader,
|
|
184
|
+
* which inlines the file contents into the snapshot before deploy upload.
|
|
185
|
+
*/
|
|
186
|
+
const SystemPromptSchema = z.union([
|
|
187
|
+
z.string().min(1),
|
|
188
|
+
z.strictObject({ file: z.string().min(1) }),
|
|
189
|
+
]);
|
|
190
|
+
/**
|
|
191
|
+
* Bedrock-style invocation endpoint config.
|
|
192
|
+
*
|
|
193
|
+
* `kind: 'invoke'` exposes `POST /agent/invoke` with multi-modal input and
|
|
194
|
+
* SSE response per `docs/plans/2026-05-25-platform-architecture.md` §6.
|
|
195
|
+
* Reserved for future endpoint kinds (`'connect'` for WebSocket bidirectional).
|
|
196
|
+
*/
|
|
197
|
+
const EndpointConfigSchema = z.strictObject({
|
|
198
|
+
kind: z.literal('invoke').optional(),
|
|
199
|
+
streaming: z.boolean().optional(),
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Deploy config — pod size + region.
|
|
203
|
+
*
|
|
204
|
+
* Lives inside the `agent` section (was top-level on the pre-merge `guuey.json`).
|
|
205
|
+
* Mirror shape on `guuey.mcp.json#mcpServer.deploy` (future) — same field set,
|
|
206
|
+
* same semantics, just attached to a different artifact.
|
|
207
|
+
*
|
|
208
|
+
* Latent fields like `tier`, `maxPods`, `idleTimeoutMinutes` exist on the
|
|
209
|
+
* AgentDeployment DDB model but are platform-managed (Reserved per design
|
|
210
|
+
* doc §14.3) — not exposed in user-facing config.
|
|
211
|
+
*/
|
|
212
|
+
const DeploySchema = z.strictObject({
|
|
213
|
+
/** Agent pod size. Canonical list lives in `./hosting.ts#AGENT_SIZES`. */
|
|
214
|
+
size: z.enum(AGENT_SIZES).optional(),
|
|
215
|
+
/** AWS region (e.g. `"us-east-1"`). Free-form; control plane enforces the live allow-list. */
|
|
216
|
+
region: z.string().min(1).optional(),
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* Auth posture for end-user invocations.
|
|
220
|
+
*
|
|
221
|
+
* - `'anonymous'` (default) — guest cookie minted on first invoke; persistent thread.
|
|
222
|
+
* - `'required'` — end-user must present a valid Cognito JWT; anonymous rejected.
|
|
223
|
+
* - `'optional'` — accept both; identity context reflects which.
|
|
224
|
+
*/
|
|
225
|
+
const AuthSchema = z.enum(['anonymous', 'required', 'optional']);
|
|
226
|
+
/**
|
|
227
|
+
* Memory model. `'thread'` = automatic conversation history (DDB).
|
|
228
|
+
* Semantic / vector memory deferred to a later schema version.
|
|
229
|
+
*/
|
|
230
|
+
const MemorySchema = z.enum(['thread', 'none']);
|
|
231
|
+
/**
|
|
232
|
+
* VFS scopes to mount into the pod. Empty array = no VFS (still uses thread + state).
|
|
233
|
+
*/
|
|
234
|
+
const StorageScopeSchema = z.array(z.enum(['user', 'app']));
|
|
235
|
+
/**
|
|
236
|
+
* The agent section — composes runtime + platform features + deploy.
|
|
237
|
+
*
|
|
238
|
+
* Exported as a zod object so the top-level `GuueyJsonV1` schema (in
|
|
239
|
+
* `./schema.ts`) can nest it. Static type via {@link GuueyAgent}.
|
|
240
|
+
*/
|
|
241
|
+
export const AgentSectionV1 = z.strictObject({
|
|
242
|
+
// ── Deploy routing ──
|
|
243
|
+
/**
|
|
244
|
+
* Routing declaration for `guuey deploy`:
|
|
245
|
+
*
|
|
246
|
+
* - `'code'` — a worker-entry project: the CLI runs the package build
|
|
247
|
+
* (`corepack pnpm build` → `guuey.worker.js`), packs the project root,
|
|
248
|
+
* and the platform builds the runtime image from its own base image
|
|
249
|
+
* (code-mode `AgentDeployment`). Stamped by `@guuey/create-agentic-app`
|
|
250
|
+
* scaffolds so they route explicitly.
|
|
251
|
+
* - `'declarative'` — no source to build; the CLI POSTs the guuey.json
|
|
252
|
+
* snapshot directly (nocode `AgentDeployment`, stock runtime pod).
|
|
253
|
+
* - absent — the platform infers: declarative when the project has no
|
|
254
|
+
* Dockerfile (a root Dockerfile keeps the legacy user-image code path).
|
|
255
|
+
*/
|
|
256
|
+
mode: z.enum(['code', 'declarative']).optional(),
|
|
257
|
+
// ── Framework + runtime ──
|
|
258
|
+
framework: z.enum(AGENT_FRAMEWORKS).optional(),
|
|
259
|
+
model: z.string().min(1).optional(),
|
|
260
|
+
/**
|
|
261
|
+
* Managed-LLM provider selector — only meaningful for
|
|
262
|
+
* `framework: 'openai-agents-sdk'`, where OpenAI and OpenRouter share the
|
|
263
|
+
* identical OpenAI wire and the Router must pick the upstream + platform key
|
|
264
|
+
* at invoke time. `'openrouter'` routes managed traffic to OpenRouter;
|
|
265
|
+
* absent or `'openai'` uses native OpenAI. Ignored for other frameworks
|
|
266
|
+
* (claude → Anthropic, google-adk → Gemini are framework-determined).
|
|
267
|
+
*/
|
|
268
|
+
modelProvider: z.enum(['openai', 'openrouter']).optional(),
|
|
269
|
+
systemPrompt: SystemPromptSchema.optional(),
|
|
270
|
+
/**
|
|
271
|
+
* MCP servers the agent may call. **Replaces** the platform default
|
|
272
|
+
* (`{ ggui: { kind: 'external', url: 'https://mcp.ggui.ai', transport: 'http' } }`)
|
|
273
|
+
* when present — not merged. Omit the block to inherit the default; include
|
|
274
|
+
* `ggui` explicitly to keep it alongside other servers.
|
|
275
|
+
*
|
|
276
|
+
* Each entry is a discriminated union on `kind`:
|
|
277
|
+
* - `'colocated'` — stdio child inside the agent pod
|
|
278
|
+
* - `'hosted'` — guuey-hosted registry MCP (Starter+)
|
|
279
|
+
* - `'proxied'` — 3rd-party SaaS via mcp-proxy (v2)
|
|
280
|
+
* - `'external'` — builder-hosted URL (plain or federated)
|
|
281
|
+
*/
|
|
282
|
+
mcpServers: z.record(z.string().min(1), McpServerSchema).optional(),
|
|
283
|
+
tools: ToolGatesSchema.optional(),
|
|
284
|
+
runtime: RuntimeConfigSchema.optional(),
|
|
285
|
+
/** Claude Agent SDK-specific knobs. Only read when `framework: 'claude-agent-sdk'`. */
|
|
286
|
+
claude: ClaudeFrameworkConfigSchema.optional(),
|
|
287
|
+
// ── Platform features (opt-in, sensible defaults) ──
|
|
288
|
+
auth: AuthSchema.optional(),
|
|
289
|
+
memory: MemorySchema.optional(),
|
|
290
|
+
storage: StorageScopeSchema.optional(),
|
|
291
|
+
// ── Env + secrets ──
|
|
292
|
+
/** Literal non-sensitive env vars baked into the pod at boot. */
|
|
293
|
+
env: z.record(z.string().min(1), z.string()).optional(),
|
|
294
|
+
/**
|
|
295
|
+
* Names (not values) of secrets the pod needs. Values are set via
|
|
296
|
+
* `guuey secrets set NAME=...`, stored KMS-encrypted in DDB. Deploy-controller
|
|
297
|
+
* resolves to values and injects as env vars at pod boot.
|
|
298
|
+
*/
|
|
299
|
+
secrets: z.array(z.string().min(1)).optional(),
|
|
300
|
+
// ── Invocation endpoint ──
|
|
301
|
+
endpoint: EndpointConfigSchema.optional(),
|
|
302
|
+
// ── Deploy ──
|
|
303
|
+
deploy: DeploySchema.optional(),
|
|
304
|
+
});
|
|
305
|
+
// ── No-literal-secrets validation (deploy-time contract enforcement) ──────────
|
|
306
|
+
//
|
|
307
|
+
// The schema (McpServerSchema JSDoc) requires secrets in `mcpServers[].headers`
|
|
308
|
+
// be referenced via `${env.NAME}` (declared in `agent.secrets`), never literal-
|
|
309
|
+
// inlined — otherwise the secret rides into the pod's `NOCODE_CONFIG_JSON` env
|
|
310
|
+
// var as plaintext (which the B6.3 secretKeyRef hardening cannot protect, since
|
|
311
|
+
// it's embedded in the config JSON, not a discrete env var). Nothing enforced
|
|
312
|
+
// this at deploy time; `validateNoLiteralSecrets` does.
|
|
313
|
+
/**
|
|
314
|
+
* Header names that carry credentials. A value here that is a bare literal (no
|
|
315
|
+
* `${env.NAME}` reference) is almost certainly a baked credential. Lowercased
|
|
316
|
+
* for case-insensitive matching. Deliberately focused on unambiguous auth
|
|
317
|
+
* headers — generic-shaped secrets in ANY header are caught separately by
|
|
318
|
+
* {@link SECRET_SHAPE_PATTERNS} (so we don't false-positive on, e.g., `Cookie`).
|
|
319
|
+
*/
|
|
320
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
321
|
+
'authorization',
|
|
322
|
+
'proxy-authorization',
|
|
323
|
+
'x-api-key',
|
|
324
|
+
'x-auth-token',
|
|
325
|
+
'x-authorization',
|
|
326
|
+
'api-key',
|
|
327
|
+
'api_key',
|
|
328
|
+
'apikey',
|
|
329
|
+
]);
|
|
330
|
+
/**
|
|
331
|
+
* Secret-shaped literal patterns — NAMED prefixes only, deliberately NOT
|
|
332
|
+
* generic entropy/length heuristics (those false-positive on legit long IDs).
|
|
333
|
+
* Applied to the header value AFTER stripping `${env.NAME}` references, so a
|
|
334
|
+
* ref-based value like `Bearer ${env.TOKEN}` never trips them.
|
|
335
|
+
*/
|
|
336
|
+
const SECRET_SHAPE_PATTERNS = [
|
|
337
|
+
/sk-ant-/, // Anthropic
|
|
338
|
+
/\bsk-[A-Za-z0-9]{20,}/, // OpenAI-style sk- keys
|
|
339
|
+
/\bsk_(live|test)_[A-Za-z0-9]{16,}/, // Stripe secret keys
|
|
340
|
+
/\bAKIA[0-9A-Z]{16}\b/, // AWS access key id
|
|
341
|
+
/\bASIA[0-9A-Z]{16}\b/, // AWS temp access key id
|
|
342
|
+
/\bghp_[A-Za-z0-9]{20,}/, // GitHub PAT
|
|
343
|
+
/\bgho_[A-Za-z0-9]{20,}/, // GitHub OAuth
|
|
344
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/, // GitHub fine-grained PAT
|
|
345
|
+
/\bxox[baprs]-[0-9A-Za-z-]{10,}/, // Slack
|
|
346
|
+
/\bglpat-[A-Za-z0-9_-]{16,}/, // GitLab PAT
|
|
347
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/, // JWT (3 b64url segments)
|
|
348
|
+
];
|
|
349
|
+
/**
|
|
350
|
+
* Is a header name credential-bearing? The explicit set plus low-false-positive
|
|
351
|
+
* NAME signals (a custom `X-Auth-*` / `*-secret` / `*-password` header is almost
|
|
352
|
+
* certainly a credential). Deliberately NOT bare `key`/`token` (benign uses:
|
|
353
|
+
* `Idempotency-Key`, `X-Request-Token`). Fully-opaque secrets in an
|
|
354
|
+
* arbitrarily-named header still slip layer 2 — that's undetectable without
|
|
355
|
+
* false-positives, so it stays best-effort (a lint, not a guarantee).
|
|
356
|
+
*/
|
|
357
|
+
function isSensitiveHeaderName(name) {
|
|
358
|
+
const n = name.toLowerCase();
|
|
359
|
+
if (SENSITIVE_HEADER_NAMES.has(n))
|
|
360
|
+
return true;
|
|
361
|
+
if (/(^|[-_])auth(orization)?([-_]|$)/.test(n))
|
|
362
|
+
return true; // x-auth-*, *-auth-token
|
|
363
|
+
if (/(^|[-_])api[-_]?key([-_]|$)/.test(n))
|
|
364
|
+
return true; // x-api-key variants
|
|
365
|
+
return /(secret|credential|password|passwd)/.test(n);
|
|
366
|
+
}
|
|
367
|
+
/** Non-secret auth scheme words that may legitimately stand before an `${env.NAME}` ref. */
|
|
368
|
+
const AUTH_SCHEME_WORDS = /\b(bearer|basic|token|digest|negotiate)\b/gi;
|
|
369
|
+
const ENV_REF_GLOBAL = /\$\{env\.[A-Za-z_][A-Za-z0-9_]*\}/g;
|
|
370
|
+
const HAS_ENV_REF = /\$\{env\.[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
371
|
+
/**
|
|
372
|
+
* Validate that no `mcpServers[*].headers` value carries a LITERAL secret.
|
|
373
|
+
* Returns a list of human-readable violation messages (empty = clean).
|
|
374
|
+
*
|
|
375
|
+
* Two layers:
|
|
376
|
+
* 1. Strip `${env.NAME}` refs from each value, then match the literal
|
|
377
|
+
* remainder against {@link SECRET_SHAPE_PATTERNS} → a baked secret in ANY
|
|
378
|
+
* header (e.g. `Authorization: Bearer sk-ant-...`).
|
|
379
|
+
* 2. For {@link SENSITIVE_HEADER_NAMES}, a value with NO `${env.NAME}` ref and
|
|
380
|
+
* a non-trivial literal (after removing scheme words) → a baked credential
|
|
381
|
+
* (e.g. `X-API-Key: abc123`, `Authorization: Basic <base64>`).
|
|
382
|
+
*
|
|
383
|
+
* Legit ref-based values (`Authorization: Bearer ${env.TOKEN}`,
|
|
384
|
+
* `X-API-Key: ${env.KEY}`) and non-secret literals (`Content-Type`) pass.
|
|
385
|
+
*/
|
|
386
|
+
export function validateNoLiteralSecrets(agent) {
|
|
387
|
+
const violations = [];
|
|
388
|
+
const servers = agent?.mcpServers;
|
|
389
|
+
if (!servers)
|
|
390
|
+
return violations;
|
|
391
|
+
for (const [serverName, server] of Object.entries(servers)) {
|
|
392
|
+
// Only `colocated` and `external` union arms carry a `headers` field.
|
|
393
|
+
const headers = 'headers' in server ? server.headers : undefined;
|
|
394
|
+
if (!headers)
|
|
395
|
+
continue;
|
|
396
|
+
for (const [headerName, rawValue] of Object.entries(headers)) {
|
|
397
|
+
const value = String(rawValue);
|
|
398
|
+
const literalRemainder = value.replace(ENV_REF_GLOBAL, '');
|
|
399
|
+
// (1) secret-shaped literal anywhere in the non-ref text.
|
|
400
|
+
if (SECRET_SHAPE_PATTERNS.some((re) => re.test(literalRemainder))) {
|
|
401
|
+
violations.push(`mcpServers.${serverName}.headers.${headerName}: contains a literal secret — reference it as \${env.NAME} and declare the name in agent.secrets`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// (2) sensitive header with a fully-literal (no-ref) credential value.
|
|
405
|
+
if (isSensitiveHeaderName(headerName) && !HAS_ENV_REF.test(value)) {
|
|
406
|
+
const bare = literalRemainder.replace(AUTH_SCHEME_WORDS, '').trim();
|
|
407
|
+
if (bare.length > 0) {
|
|
408
|
+
violations.push(`mcpServers.${serverName}.headers.${headerName}: sensitive header must reference a secret as \${env.NAME} (declared in agent.secrets), not a literal value`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return violations;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Platform default MCP server map. Applied by the pod when `agent.mcpServers`
|
|
417
|
+
* is absent. Exposed here so non-pod consumers (CLI dry-run, lints) can show
|
|
418
|
+
* the effective shape without duplicating the literal.
|
|
419
|
+
*
|
|
420
|
+
* The ggui server is `kind: 'external'` — it is builder-declared when present
|
|
421
|
+
* or injected by the platform at runtime. Federation still detects it by host
|
|
422
|
+
* (via `isGguiUrl`) regardless of which key it's declared under.
|
|
423
|
+
*/
|
|
424
|
+
export const DEFAULT_AGENT_MCP_SERVERS = {
|
|
425
|
+
ggui: { kind: 'external', url: 'https://mcp.ggui.ai', transport: 'http' },
|
|
426
|
+
};
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guuey.json#app` — the App Store / Portal listing section.
|
|
3
|
+
*
|
|
4
|
+
* Describes how the deployable surfaces to end-users in Portal Discover
|
|
5
|
+
* and (optionally) at a custom domain. Read by the platform on first
|
|
6
|
+
* deploy to upsert an `AgentListing` row.
|
|
7
|
+
*
|
|
8
|
+
* Lives at the same level as `agent` and `ggui` sections — the artifact
|
|
9
|
+
* (agent or mcp-server) is what's deployable; this section is presentation.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
/**
|
|
13
|
+
* The app section schema. All fields optional in v1 — a project may carry
|
|
14
|
+
* the bare minimum at first and grow the listing as it publishes.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AppSectionV1: z.ZodObject<{
|
|
17
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
18
|
+
name: z.ZodOptional<z.ZodString>;
|
|
19
|
+
description: z.ZodOptional<z.ZodString>;
|
|
20
|
+
iconUrl: z.ZodOptional<z.ZodURL>;
|
|
21
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
|
+
customDomain: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
/** Static TypeScript type for the app section. */
|
|
25
|
+
export type GuueyApp = z.infer<typeof AppSectionV1>;
|
|
26
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0CxB;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;;;kBAOvB,CAAC;AAEH,kDAAkD;AAClD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guuey.json#app` — the App Store / Portal listing section.
|
|
3
|
+
*
|
|
4
|
+
* Describes how the deployable surfaces to end-users in Portal Discover
|
|
5
|
+
* and (optionally) at a custom domain. Read by the platform on first
|
|
6
|
+
* deploy to upsert an `AgentListing` row.
|
|
7
|
+
*
|
|
8
|
+
* Lives at the same level as `agent` and `ggui` sections — the artifact
|
|
9
|
+
* (agent or mcp-server) is what's deployable; this section is presentation.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
/**
|
|
13
|
+
* Slug used for the public URL and App Store listing. Forms part of the
|
|
14
|
+
* agent's reachable hostname: `<slug>.agents.<env>.guuey.com`.
|
|
15
|
+
*
|
|
16
|
+
* Slug uniqueness is enforced platform-side via the `SlugClaim` model.
|
|
17
|
+
* Matches `[a-z0-9][a-z0-9-]{1,62}` — lowercase, hyphens, no leading dash.
|
|
18
|
+
*/
|
|
19
|
+
const SlugSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.min(2)
|
|
22
|
+
.max(63)
|
|
23
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, 'must be lowercase alphanumeric with optional internal hyphens');
|
|
24
|
+
/**
|
|
25
|
+
* Tag for App Store discovery. Free-form short strings; no curated taxonomy
|
|
26
|
+
* in α — Portal renders them as plain text chips for now.
|
|
27
|
+
*/
|
|
28
|
+
const TagSchema = z.string().min(1).max(32);
|
|
29
|
+
/**
|
|
30
|
+
* Custom domain for the agent (e.g. `chef.example.com`). Optional.
|
|
31
|
+
*
|
|
32
|
+
* α requires explicit lifecycle: `guuey domain add <fqdn>` → user configures
|
|
33
|
+
* CNAME at registrar → `guuey domain verify <fqdn>` → platform requests
|
|
34
|
+
* per-domain ACM cert + attaches Ingress rule. Just setting this field does
|
|
35
|
+
* NOT auto-provision — the CLI warns when set without an attached domain
|
|
36
|
+
* record (see design doc §10.3).
|
|
37
|
+
*/
|
|
38
|
+
const CustomDomainSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.min(4)
|
|
41
|
+
.max(253)
|
|
42
|
+
.regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i, 'must be a valid fully-qualified domain name');
|
|
43
|
+
/**
|
|
44
|
+
* The app section schema. All fields optional in v1 — a project may carry
|
|
45
|
+
* the bare minimum at first and grow the listing as it publishes.
|
|
46
|
+
*/
|
|
47
|
+
export const AppSectionV1 = z.strictObject({
|
|
48
|
+
slug: SlugSchema.optional(),
|
|
49
|
+
name: z.string().min(1).max(120).optional(),
|
|
50
|
+
description: z.string().min(1).max(500).optional(),
|
|
51
|
+
iconUrl: z.url().optional(),
|
|
52
|
+
tags: z.array(TagSchema).max(10).optional(),
|
|
53
|
+
customDomain: CustomDomainSchema.optional(),
|
|
54
|
+
});
|
package/dist/ggui.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guuey.json#ggui` — optional cross-protocol integration section.
|
|
3
|
+
*
|
|
4
|
+
* Reserved for projects that use the ggui protocol (mcp.ggui.ai) for
|
|
5
|
+
* rendering. The actual ggui-app identity (slug, gadgets, publicEnv,
|
|
6
|
+
* etc.) lives in a separate `ggui.json` file owned by the ggui
|
|
7
|
+
* ecosystem (`@ggui-ai/project-config`). This section provides the
|
|
8
|
+
* reference from `guuey.json` to that sibling file.
|
|
9
|
+
*
|
|
10
|
+
* Two reference forms:
|
|
11
|
+
*
|
|
12
|
+
* - **File path** — `{ configFile: './ggui.json' }` resolves the sibling
|
|
13
|
+
* file at deploy time. Recommended for most projects.
|
|
14
|
+
* - **Inline** — `{ inline: { ...ggui-json-fields } }` for projects that
|
|
15
|
+
* prefer a single config file. Schema validation defers to the ggui
|
|
16
|
+
* ecosystem (we only type the wrapper).
|
|
17
|
+
*
|
|
18
|
+
* Cross-ecosystem boundary: guuey-side does NOT validate the ggui section's
|
|
19
|
+
* contents. The platform is MCP-server-agnostic — agents can use
|
|
20
|
+
* `mcp.ggui.ai` and get rendering for free, or use a different MCP server
|
|
21
|
+
* and skip this section entirely.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
/**
|
|
25
|
+
* The ggui integration section schema. Mutually exclusive forms:
|
|
26
|
+
* `configFile` reference OR `inline` object. Both optional — projects
|
|
27
|
+
* that don't use ggui rendering omit the whole `ggui` block.
|
|
28
|
+
*/
|
|
29
|
+
export declare const GguiSectionV1: z.ZodObject<{
|
|
30
|
+
appId: z.ZodOptional<z.ZodString>;
|
|
31
|
+
configFile: z.ZodOptional<z.ZodString>;
|
|
32
|
+
inline: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
/** Static TypeScript type for the ggui integration section. */
|
|
35
|
+
export type GuueyGguiSection = z.infer<typeof GguiSectionV1>;
|
|
36
|
+
//# sourceMappingURL=ggui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ggui.d.ts","sourceRoot":"","sources":["../src/ggui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;kBAyBvB,CAAC;AAEJ,+DAA+D;AAC/D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
package/dist/ggui.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guuey.json#ggui` — optional cross-protocol integration section.
|
|
3
|
+
*
|
|
4
|
+
* Reserved for projects that use the ggui protocol (mcp.ggui.ai) for
|
|
5
|
+
* rendering. The actual ggui-app identity (slug, gadgets, publicEnv,
|
|
6
|
+
* etc.) lives in a separate `ggui.json` file owned by the ggui
|
|
7
|
+
* ecosystem (`@ggui-ai/project-config`). This section provides the
|
|
8
|
+
* reference from `guuey.json` to that sibling file.
|
|
9
|
+
*
|
|
10
|
+
* Two reference forms:
|
|
11
|
+
*
|
|
12
|
+
* - **File path** — `{ configFile: './ggui.json' }` resolves the sibling
|
|
13
|
+
* file at deploy time. Recommended for most projects.
|
|
14
|
+
* - **Inline** — `{ inline: { ...ggui-json-fields } }` for projects that
|
|
15
|
+
* prefer a single config file. Schema validation defers to the ggui
|
|
16
|
+
* ecosystem (we only type the wrapper).
|
|
17
|
+
*
|
|
18
|
+
* Cross-ecosystem boundary: guuey-side does NOT validate the ggui section's
|
|
19
|
+
* contents. The platform is MCP-server-agnostic — agents can use
|
|
20
|
+
* `mcp.ggui.ai` and get rendering for free, or use a different MCP server
|
|
21
|
+
* and skip this section entirely.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
/**
|
|
25
|
+
* The ggui integration section schema. Mutually exclusive forms:
|
|
26
|
+
* `configFile` reference OR `inline` object. Both optional — projects
|
|
27
|
+
* that don't use ggui rendering omit the whole `ggui` block.
|
|
28
|
+
*/
|
|
29
|
+
export const GguiSectionV1 = z
|
|
30
|
+
.strictObject({
|
|
31
|
+
/**
|
|
32
|
+
* The bound ggui app id — the federation `aud` target. ggui provisions
|
|
33
|
+
* the `GguiApp` and hands the builder this fixed id (federation contract
|
|
34
|
+
* §4 / `docs/plans/2026-06-14-ggui-guuey-identity-federation.md`). The
|
|
35
|
+
* pod addresses `mcp.ggui.ai/apps/<appId>` and mints tokens with
|
|
36
|
+
* `aud = https://mcp.ggui.ai/apps/<appId>`. Omit to use `mcp.ggui.ai`
|
|
37
|
+
* without per-app federation (no minted token).
|
|
38
|
+
*/
|
|
39
|
+
appId: z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^[A-Za-z0-9_-]{1,64}$/, 'ggui.appId must be a ggui app id ([A-Za-z0-9_-], ≤64)')
|
|
42
|
+
.optional(),
|
|
43
|
+
configFile: z.string().min(1).optional(),
|
|
44
|
+
/**
|
|
45
|
+
* Inline ggui config (opaque to this package). The actual schema
|
|
46
|
+
* lives in `@ggui-ai/project-config`; we type it as a record here
|
|
47
|
+
* to avoid coupling the OSS guuey config package to the ggui SDK.
|
|
48
|
+
*/
|
|
49
|
+
inline: z.record(z.string(), z.unknown()).optional(),
|
|
50
|
+
})
|
|
51
|
+
.refine((val) => !(val.configFile && val.inline), { message: '`ggui` cannot specify both `configFile` and `inline` — pick one' });
|