auggy 0.3.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Augment resolver — AugmentConfig[] → Augment[].
3
+ *
4
+ * Maps each augment declaration from agent.yaml to a concrete Augment
5
+ * object by dispatching to the appropriate factory function. Built-in
6
+ * augments resolve by type name; custom augments resolve by dynamic
7
+ * import of a local .ts file.
8
+ *
9
+ * Special handling:
10
+ * - supabaseMemory: constructs a SupabaseLikeClient from supabaseUrl
11
+ * + supabaseKey options via @supabase/supabase-js.
12
+ * - fileMemory, filesystem: resolves relative paths against agentDir.
13
+ * - All augments: overrides the auto-generated augment name with the
14
+ * operator's chosen instance name from the config.
15
+ */
16
+
17
+ import { resolve } from "node:path";
18
+ import { pathToFileURL } from "node:url";
19
+ import { fileMemory } from "../augments/file-memory";
20
+ import { supabaseMemory } from "../augments/supabase-memory";
21
+ import { filesystem } from "../augments/filesystem";
22
+ import { webTransport } from "../transports/web-transport";
23
+ import { webFetch } from "../augments/web-fetch";
24
+ import { orgContext } from "../augments/org-context";
25
+ import { skills } from "../augments/skills";
26
+ import { bash } from "../augments/bash";
27
+ import { notify } from "../augments/notify";
28
+ import { telegramTransport } from "../augments/telegram-transport";
29
+ import { turnControl, type TurnControlOptions } from "../augments/turn-control";
30
+ import { visitorAuth } from "../augments/visitor-auth";
31
+ import type { VisitorAuthOptions, VisitorAuthAugmentExtras } from "../augments/visitor-auth/types";
32
+ import { link } from "../augments/link";
33
+ import type { LinkAugmentAgentCard, LinkAugmentOptions, LinkPeerConfig } from "../augments/link";
34
+ import type { Augment, NotifyAugmentOptions, TelegramTransportOptions } from "../types";
35
+ import type { AugmentConfig } from "./types";
36
+ import type { BudgetsAugmentOptions } from "../augments/budgets";
37
+ import { validateBundledSkills } from "./skill-validator";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Path resolution helper
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function resolvePath(path: string, agentDir: string): string {
44
+ if (path.startsWith("/")) return path;
45
+ return resolve(agentDir, path);
46
+ }
47
+
48
+ /**
49
+ * Resolve an orgContext baseUrl, normalizing relative `file://...` shapes
50
+ * against the agent dir so the augment factory only ever sees absolute
51
+ * file:// URLs.
52
+ *
53
+ * Accepted inputs:
54
+ * - `http://...` / `https://...` — passed through unchanged
55
+ * - `file:///abs/path` — passed through unchanged (already absolute)
56
+ * - `file://./relative/path` — resolved against agentDir, returned as
57
+ * an absolute file:// URL via `pathToFileURL`
58
+ * - `file://relative/path` — same; tolerated for ergonomics. The two-
59
+ * slash relative form mirrors how operators tend to write `file://`-style
60
+ * URLs in YAML config (`file://./org-context`).
61
+ *
62
+ * Rationale: keeping the relative→absolute conversion in the resolver avoids
63
+ * threading an `agentDir` construction parameter through to the augment
64
+ * factory (per ADR-024 — no new kernel surface; per the org-context augment's
65
+ * design — the factory accepts only absolute file:// URLs).
66
+ */
67
+ function resolveOrgContextBaseUrl(baseUrl: string, agentDir: string): string {
68
+ if (!/^file:/i.test(baseUrl)) return baseUrl;
69
+
70
+ // Distinguishing absolute vs relative after stripping the `file:` scheme
71
+ // is ambiguous — both forms can produce a leading slash. So we count
72
+ // leading slashes BEFORE stripping:
73
+ // - `file:///abs/path` (three slashes) — POSIX-form absolute URL
74
+ // - `file:/abs/path` (one slash, no `//`) — uncommon but valid absolute
75
+ // - `file://./rel` (two slashes + `.`) — relative; this codebase's
76
+ // convention for "relative to agent dir"
77
+ // - `file://rel/path` (two slashes, no `.`) — also relative; tolerated
78
+ // for ergonomics (mirrors how operators write the URL in YAML config)
79
+ const afterScheme = baseUrl.replace(/^file:/i, "");
80
+ const isAbsoluteFileUrl =
81
+ afterScheme.startsWith("///") || (afterScheme.startsWith("/") && !afterScheme.startsWith("//"));
82
+
83
+ if (isAbsoluteFileUrl) {
84
+ // Already absolute — pass through unchanged.
85
+ return baseUrl;
86
+ }
87
+
88
+ // Relative form. Compute the absolute path against agentDir and return as a
89
+ // proper file:// URL.
90
+ const relPath = afterScheme.replace(/^\/+/, "");
91
+ const absPath = resolve(agentDir, relPath);
92
+ return pathToFileURL(absPath).href;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Built-in resolvers
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function resolveFileMemory(opts: Record<string, unknown>, agentDir: string): Augment {
100
+ return fileMemory({
101
+ label: opts.label as string,
102
+ source: resolvePath(opts.source as string, agentDir),
103
+ mutable: opts.mutable as boolean,
104
+ origin: opts.origin as "operator" | "system" | "agent" | "peer-derived",
105
+ priority: opts.priority as "required" | "high" | "normal" | "low" | "evictable",
106
+ placement: opts.placement as "system" | "preamble" | "assistant-preamble",
107
+ eviction: opts.eviction as "never" | "summarize" | "drop",
108
+ ttl: opts.ttl as "turn" | "session" | "persistent" | undefined,
109
+ });
110
+ }
111
+
112
+ async function resolveLayeredMemory(
113
+ opts: Record<string, unknown>,
114
+ agentDir: string,
115
+ ): Promise<Augment> {
116
+ const { layeredMemory } = await import("../augments/layered-memory");
117
+ const backend = (opts.backend as string | undefined) ?? "sqlite";
118
+ const namespace = (opts.namespace as string | undefined) ?? "ep";
119
+ const retentionDays = opts.retentionDays as number | undefined;
120
+
121
+ if (backend === "sqlite") {
122
+ const dbPath = opts.dbPath as string | undefined;
123
+ return layeredMemory({
124
+ backend: "sqlite",
125
+ dbPath: dbPath ? resolvePath(dbPath, agentDir) : resolvePath("./memory.db", agentDir),
126
+ namespace,
127
+ retentionDays,
128
+ });
129
+ }
130
+
131
+ if (backend === "supabase") {
132
+ const supabaseUrl = opts.supabaseUrl as string | undefined;
133
+ const supabaseKey = opts.supabaseKey as string | undefined;
134
+ if (!supabaseUrl || !supabaseKey) {
135
+ throw new Error(
136
+ "layeredMemory: supabase backend requires supabaseUrl and supabaseKey options",
137
+ );
138
+ }
139
+ const { createClient } = await import("@supabase/supabase-js");
140
+ const client = createClient(supabaseUrl, supabaseKey) as unknown as Parameters<
141
+ typeof layeredMemory
142
+ >[0]["client"];
143
+ return layeredMemory({
144
+ backend: "supabase",
145
+ client,
146
+ table: (opts.table as string | undefined) ?? "agent_memory",
147
+ namespace,
148
+ retentionDays,
149
+ });
150
+ }
151
+
152
+ throw new Error(`layeredMemory: unknown backend "${backend}"`);
153
+ }
154
+
155
+ async function resolveSupabaseMemory(opts: Record<string, unknown>): Promise<Augment> {
156
+ const { supabaseUrl, supabaseKey, ...rest } = opts;
157
+ if (typeof supabaseUrl !== "string" || typeof supabaseKey !== "string") {
158
+ throw new Error(
159
+ "supabaseMemory requires supabaseUrl and supabaseKey options (use ${ENV_VAR} interpolation)",
160
+ );
161
+ }
162
+
163
+ // Lazy import — only load @supabase/supabase-js when supabaseMemory is used.
164
+ // The real SupabaseClient has narrower types than SupabaseLikeClient
165
+ // (e.g. data is null on error), so we cast through unknown.
166
+ const { createClient } = await import("@supabase/supabase-js");
167
+ const client = createClient(supabaseUrl, supabaseKey) as unknown as Parameters<
168
+ typeof supabaseMemory
169
+ >[0]["client"];
170
+
171
+ return supabaseMemory({
172
+ namespace: rest.namespace as string,
173
+ client,
174
+ table: rest.table as string,
175
+ mutable: rest.mutable as boolean,
176
+ origin: rest.origin as "operator" | "system" | "agent" | "peer-derived",
177
+ priority: rest.priority as "required" | "high" | "normal" | "low" | "evictable",
178
+ placement: rest.placement as "system" | "preamble" | "assistant-preamble",
179
+ eviction: rest.eviction as "never" | "summarize" | "drop",
180
+ searchLimit: rest.searchLimit as number | undefined,
181
+ });
182
+ }
183
+
184
+ function resolveFilesystem(opts: Record<string, unknown>, agentDir: string): Augment {
185
+ const mounts = (opts.mounts as Array<Record<string, unknown>>).map((m) => ({
186
+ name: m.name as string,
187
+ path: resolvePath(m.path as string, agentDir),
188
+ writable: m.writable as boolean | undefined,
189
+ deletable: m.deletable as boolean | undefined,
190
+ maxReadSize: m.maxReadSize as number | undefined,
191
+ maxWriteSize: m.maxWriteSize as number | undefined,
192
+ searchExcludes: m.searchExcludes as string[] | undefined,
193
+ }));
194
+
195
+ return filesystem({
196
+ mounts,
197
+ skillFile: opts.skillFile ? resolvePath(opts.skillFile as string, agentDir) : undefined,
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Resolve the built-in `skills` augment (ADR-030). The `dir` option is
203
+ * resolved against agentDir using the same relative→absolute pattern as
204
+ * other agent-dir-relative paths, then handed to the augment factory.
205
+ *
206
+ * Default `dir` is `./skills` to match the scaffold layout (`auggy create`
207
+ * copies bundled skill folders to `<agentDir>/skills/<augment>/`).
208
+ */
209
+ function resolveSkills(opts: Record<string, unknown>, agentDir: string): Augment {
210
+ const rawDir = (opts.dir as string | undefined) ?? "./skills";
211
+ return skills({ dir: resolvePath(rawDir, agentDir) });
212
+ }
213
+
214
+ function resolveWebTransport(
215
+ opts: Record<string, unknown>,
216
+ lateBindings: { revocationCheck: ((id: string) => boolean) | null },
217
+ ): Augment {
218
+ const vtBase = opts.visitorTokens as
219
+ | { enabled?: boolean; ttlSeconds?: number; signingKey?: string; agentBinding?: string }
220
+ | undefined;
221
+ return webTransport({
222
+ port: opts.port as number,
223
+ auth: opts.auth as { type: "bearer"; token: string },
224
+ cors: opts.cors as { origins: string[] } | undefined,
225
+ maxMessageLength: opts.maxMessageLength as number | undefined,
226
+ access: opts.access as { agents?: Array<{ id: string; sharedSecret: string }> } | undefined,
227
+ concurrency: opts.concurrency as number | undefined,
228
+ maxQueueDepth: opts.maxQueueDepth as number | undefined,
229
+ rateLimitPerPeer: opts.rateLimitPerPeer as { maxPerMinute: number } | undefined,
230
+ visitorTokens: vtBase
231
+ ? {
232
+ ...vtBase,
233
+ revocationCheck: (id: string) => lateBindings.revocationCheck?.(id) ?? false,
234
+ }
235
+ : undefined,
236
+ });
237
+ }
238
+
239
+ function resolveWebFetch(opts: Record<string, unknown>): Augment {
240
+ return webFetch({
241
+ timeoutMs: opts.timeoutMs as number | undefined,
242
+ maxRedirects: opts.maxRedirects as number | undefined,
243
+ userAgent: opts.userAgent as string | undefined,
244
+ defaultHeaders: opts.defaultHeaders as Record<string, string> | undefined,
245
+ });
246
+ }
247
+
248
+ async function resolveCustom(config: AugmentConfig, agentDir: string): Promise<Augment> {
249
+ if (!config.source) {
250
+ throw new Error(`Custom augment "${config.name}": source path is required`);
251
+ }
252
+
253
+ const absPath = resolvePath(config.source, agentDir);
254
+ let mod: Record<string, unknown>;
255
+ try {
256
+ mod = await import(absPath);
257
+ } catch (err) {
258
+ throw new Error(
259
+ `Custom augment "${config.name}": failed to import "${absPath}": ${(err as Error).message}`,
260
+ );
261
+ }
262
+
263
+ const factory = mod.default;
264
+ if (typeof factory !== "function") {
265
+ throw new Error(
266
+ `Custom augment "${config.name}": "${absPath}" must have a default export that is a function (got ${typeof factory})`,
267
+ );
268
+ }
269
+
270
+ return factory(config.options ?? {}) as Augment;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Public API
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function resolveBash(opts: Record<string, unknown>, agentDir: string): Augment {
278
+ const scripts = (opts.scripts as Array<Record<string, unknown>> | undefined)?.map((s) => ({
279
+ name: s.name as string,
280
+ description: s.description as string,
281
+ command: s.command as string,
282
+ workingDir: s.workingDir ? resolvePath(s.workingDir as string, agentDir) : undefined,
283
+ timeout: s.timeout as number | undefined,
284
+ }));
285
+
286
+ return bash({
287
+ risk: opts.risk as "scripts-only" | "restricted" | "standard" | "unrestricted" | undefined,
288
+ allowedCommands: opts.allowedCommands as string[] | undefined,
289
+ blockedCommands: opts.blockedCommands as string[] | undefined,
290
+ workingDir: opts.workingDir ? resolvePath(opts.workingDir as string, agentDir) : undefined,
291
+ inheritEnv: opts.inheritEnv as boolean | undefined,
292
+ env: opts.env as Record<string, string> | undefined,
293
+ timeout: opts.timeout as number | undefined,
294
+ maxOutputBytes: opts.maxOutputBytes as number | undefined,
295
+ maxToolCallsPerTurn: opts.maxToolCallsPerTurn as number | undefined,
296
+ scripts,
297
+ });
298
+ }
299
+
300
+ function resolveLink(opts: Record<string, unknown>, agentDir: string): Augment {
301
+ const card = opts.agentCard as Record<string, unknown>;
302
+ const agentCard: LinkAugmentAgentCard = {
303
+ id: card.id as string,
304
+ name: card.name as string,
305
+ description: card.description as string,
306
+ endpointUrl: card.endpointUrl as string,
307
+ capabilities: card.capabilities as string[] | undefined,
308
+ };
309
+
310
+ const peersRaw = (opts.peers as Record<string, Record<string, unknown>> | undefined) ?? {};
311
+ const peers: Record<string, LinkPeerConfig> = {};
312
+ for (const [name, p] of Object.entries(peersRaw)) {
313
+ peers[name] = {
314
+ url: p.url as string,
315
+ bearer: p.bearer as string,
316
+ participantId: p.participantId as string,
317
+ inboundBearer: p.inboundBearer as string,
318
+ inboundBearerId: p.inboundBearerId as string,
319
+ };
320
+ }
321
+
322
+ const linkOpts: LinkAugmentOptions = {
323
+ port: opts.port as number | undefined,
324
+ dbPath: resolvePath(opts.dbPath as string, agentDir),
325
+ agentCard,
326
+ peers,
327
+ };
328
+ return link(linkOpts);
329
+ }
330
+
331
+ function resolveVisitorAuth(opts: Record<string, unknown>, agentDir: string): Augment {
332
+ const dbPath = (opts.dbPath as string | undefined) ?? "./visitor-auth.db";
333
+ // CRITICAL: distinguish `null` (operator opt-out) from `undefined` (defaults to ./memory.db).
334
+ // Using ?? would coerce both to the default string — wrong for opt-out semantics.
335
+ const layeredMemoryDbPath =
336
+ opts.layeredMemoryDbPath === null
337
+ ? null
338
+ : ((opts.layeredMemoryDbPath as string | undefined) ?? "./memory.db");
339
+
340
+ const config: VisitorAuthOptions = {
341
+ publicUrl: opts.publicUrl as string,
342
+ dbPath: resolvePath(dbPath, agentDir),
343
+ agentMail: opts.agentMail as VisitorAuthOptions["agentMail"],
344
+ signingKey: opts.signingKey as string,
345
+ agentBinding: opts.agentBinding as string | undefined,
346
+ rateLimit: opts.rateLimit as VisitorAuthOptions["rateLimit"],
347
+ reverifyAfterDays: opts.reverifyAfterDays as number | undefined,
348
+ tokenTtlMinutes: opts.tokenTtlMinutes as number | undefined,
349
+ notifyOnFirstVerify: opts.notifyOnFirstVerify as VisitorAuthOptions["notifyOnFirstVerify"],
350
+ layeredMemoryDbPath:
351
+ layeredMemoryDbPath === null ? null : resolvePath(layeredMemoryDbPath, agentDir),
352
+ };
353
+ return visitorAuth(config);
354
+ }
355
+
356
+ /**
357
+ * Resolve an array of augment configs into concrete Augment objects.
358
+ * Built-in types dispatch to their factory functions; custom types
359
+ * use dynamic import of local .ts files.
360
+ */
361
+ export async function resolveAugments(
362
+ configs: AugmentConfig[],
363
+ agentDir: string,
364
+ ): Promise<Augment[]> {
365
+ const augments: Augment[] = [];
366
+
367
+ // Deferred-closure for C1: webTransport gets a stable callback reference
368
+ // before visitorAuth is resolved; the callback reads lateBindings.revocationCheck
369
+ // which is populated after the loop completes.
370
+ const lateBindings: { revocationCheck: ((id: string) => boolean) | null } = {
371
+ revocationCheck: null,
372
+ };
373
+
374
+ // Fix F2 — single-source signingKey + conservative handling of operator's
375
+ // explicit `enabled` setting.
376
+ //
377
+ // visitorAuth is the sole authority for signingKey: it mints tokens so it
378
+ // MUST own the key. webTransport only verifies them; receiving the key via
379
+ // injection avoids operators having to duplicate the secret across two
380
+ // config blocks (where a mismatch silently breaks the flow).
381
+ //
382
+ // Auto-defaulting rules:
383
+ // - When visitorAuth is absent: auto-disable ONLY when operator left enabled
384
+ // unset. Explicit enabled: true is respected (custom minter scenario).
385
+ // - When visitorAuth is present: inject signingKey + auto-enable ONLY when
386
+ // operator did not explicitly set enabled: false. Explicit false is
387
+ // respected (unusual but legal).
388
+ //
389
+ // Iterates ALL webTransport configs (not just the first) so a multi-
390
+ // transport setup gets consistent injection (fixes Codex C-H2).
391
+ {
392
+ const vaConfig = configs.find((c) => c.type === "visitorAuth");
393
+ const wtConfigs = configs.filter((c) => c.type === "webTransport");
394
+
395
+ for (const wtConfig of wtConfigs) {
396
+ const wtOpts = (wtConfig.options ?? {}) as Record<string, unknown>;
397
+ const vt = (wtOpts.visitorTokens ?? {}) as Record<string, unknown>;
398
+
399
+ // Track whether operator explicitly set `enabled` (truthy or false) vs left it undefined.
400
+ const enabledExplicit = "enabled" in vt;
401
+
402
+ if (!vaConfig) {
403
+ // No visitorAuth mounted.
404
+ // If signingKey is set, warn about potential identity loss (operator
405
+ // may have removed visitorAuth between boots, stranding issued tokens).
406
+ if (vt.signingKey !== undefined) {
407
+ console.warn(
408
+ `[augment-resolver] webTransport.visitorTokens.signingKey is set but no visitorAuth augment is mounted. Pre-existing visitor tokens may not be verified (no minter is registered). If you previously had visitorAuth mounted and removed it, all verified visitors will revert to anonymous on next request.`,
409
+ );
410
+ }
411
+
412
+ if (!enabledExplicit) {
413
+ // Operator left enabled unset → auto-disable (no minter mounted).
414
+ vt.enabled = false;
415
+ wtOpts.visitorTokens = vt;
416
+ wtConfig.options = wtOpts;
417
+ } else if (vt.enabled === true) {
418
+ // Operator explicitly opted in without visitorAuth. Warn — likely a
419
+ // misconfig that previously silently worked via ephemeral fallback.
420
+ console.warn(
421
+ `[augment-resolver] webTransport.visitorTokens.enabled is true but no visitorAuth augment is mounted. Tokens will not be minted by visitorAuth; if you have a custom token-minter, set visitorTokens.signingKey explicitly. Otherwise, set enabled: false or mount visitorAuth.`,
422
+ );
423
+ }
424
+ // else: enabled: false explicitly set — nothing to do.
425
+ continue;
426
+ }
427
+
428
+ // visitorAuth IS mounted.
429
+ const vaSigningKey = (vaConfig.options as Record<string, unknown> | undefined)?.signingKey as
430
+ | string
431
+ | undefined;
432
+
433
+ if (vt.enabled === false) {
434
+ // Operator explicitly disabled visitor tokens despite mounting visitorAuth.
435
+ // Respect — visitorAuth's request_auth tool still works, but webTransport
436
+ // won't honor any minted token. Unusual but legal.
437
+ console.warn(
438
+ `[augment-resolver] visitorAuth is mounted but webTransport.visitorTokens.enabled is explicitly false. Verified visitors will not be recognized at the wire. Remove the explicit false to enable visitor recognition.`,
439
+ );
440
+ continue;
441
+ }
442
+
443
+ // Normal path: visitorAuth mounted, enabled is true (or undefined → default to true).
444
+ if (vt.signingKey !== undefined && vt.signingKey !== vaSigningKey) {
445
+ console.warn(
446
+ "[augment-resolver] webTransport.visitorTokens.signingKey is set but visitorAuth.signingKey takes precedence. Remove the duplicate from webTransport's config.",
447
+ );
448
+ }
449
+ // Inject visitorAuth's signingKey and enable visitor tokens.
450
+ vt.signingKey = vaSigningKey;
451
+ vt.enabled = true;
452
+ wtOpts.visitorTokens = vt;
453
+ wtConfig.options = wtOpts;
454
+ }
455
+ }
456
+
457
+ for (const config of configs) {
458
+ const opts = config.options ?? {};
459
+ let augment: Augment;
460
+
461
+ switch (config.type) {
462
+ case "fileMemory":
463
+ augment = resolveFileMemory(opts, agentDir);
464
+ break;
465
+ case "supabaseMemory":
466
+ augment = await resolveSupabaseMemory(opts);
467
+ break;
468
+ case "layeredMemory":
469
+ augment = await resolveLayeredMemory(opts, agentDir);
470
+ break;
471
+ case "filesystem":
472
+ augment = resolveFilesystem(opts, agentDir);
473
+ break;
474
+ case "webTransport":
475
+ augment = resolveWebTransport(opts, lateBindings);
476
+ break;
477
+ case "webFetch":
478
+ augment = resolveWebFetch(opts);
479
+ break;
480
+ case "orgContext":
481
+ augment = orgContext({
482
+ baseUrl: resolveOrgContextBaseUrl(opts.baseUrl as string, agentDir),
483
+ token: opts.token as string | undefined,
484
+ cacheTtlMs: opts.cacheTtlMs as number | undefined,
485
+ });
486
+ break;
487
+ case "skills":
488
+ augment = resolveSkills(opts, agentDir);
489
+ break;
490
+ case "bash":
491
+ augment = resolveBash(opts, agentDir);
492
+ break;
493
+ case "budgets": {
494
+ const { budgets } = await import("../augments/budgets");
495
+ const dbPath = (opts.dbPath as string | undefined) ?? "./budgets.db";
496
+ augment = budgets({
497
+ dbPath: resolvePath(dbPath, agentDir),
498
+ caps: opts.caps as BudgetsAugmentOptions["caps"],
499
+ anonymousGlobalLimit: opts.anonymousGlobalLimit as number | undefined,
500
+ dailyBudgetUsd: opts.dailyBudgetUsd as number | undefined,
501
+ cleanupWindowMs: opts.cleanupWindowMs as number | undefined,
502
+ });
503
+ break;
504
+ }
505
+ case "notify": {
506
+ augment = notify({
507
+ destinations: opts.destinations as NotifyAugmentOptions["destinations"],
508
+ rateLimit: opts.rateLimit as NotifyAugmentOptions["rateLimit"],
509
+ });
510
+ break;
511
+ }
512
+ case "telegramTransport":
513
+ augment = telegramTransport(opts as unknown as TelegramTransportOptions);
514
+ break;
515
+ case "turnControl":
516
+ augment = turnControl(opts as TurnControlOptions);
517
+ break;
518
+ case "visitorAuth":
519
+ augment = resolveVisitorAuth(opts, agentDir);
520
+ break;
521
+ case "link":
522
+ augment = resolveLink(opts, agentDir);
523
+ break;
524
+ case "custom":
525
+ augment = await resolveCustom(config, agentDir);
526
+ break;
527
+ default:
528
+ throw new Error(`Unknown augment type: "${config.type}"`);
529
+ }
530
+
531
+ // Override the auto-generated augment name with the operator's choice.
532
+ augment = { ...augment, name: config.name };
533
+ augments.push(augment);
534
+ }
535
+
536
+ // Fix C1: wire the visitorAuth revocation check into webTransport's
537
+ // deferred closure. The closure was passed to webTransport during the loop
538
+ // (before visitorAuth was necessarily resolved); populating lateBindings
539
+ // now makes the check active for all subsequent requests.
540
+ //
541
+ // Use index-based lookup (configs[i] → augments[i]) instead of name-based
542
+ // search so that operator-renamed visitorAuth augments (e.g. `name: my-auth`
543
+ // in agent.yaml) still get wired correctly. The `.name` property is
544
+ // overwritten with the config name at line 396 after each factory returns,
545
+ // so `augments.find(a => a.name === "visitor-auth")` would fail for any
546
+ // non-default name and silently disable revocation.
547
+ const vaIdx = configs.findIndex((c) => c.type === "visitorAuth");
548
+ const va = vaIdx >= 0 ? (augments[vaIdx] as Augment & VisitorAuthAugmentExtras) : undefined;
549
+ if (va?.isVisitorRevoked) {
550
+ lateBindings.revocationCheck = va.isVisitorRevoked.bind(va);
551
+ }
552
+
553
+ // Fix F18: throw when multiple visitorAuth augments are declared.
554
+ // Both would attempt to register GET/POST /visitor-auth/verify routes
555
+ // (the route-collector hard-fails on duplicate registration anyway), and
556
+ // only the first's revocation state would be visible to webTransport.
557
+ // A hard error here is more honest than a warning for a state that's
558
+ // unreachable at runtime.
559
+ const vaCount = configs.filter((c) => c.type === "visitorAuth").length;
560
+ if (vaCount > 1) {
561
+ throw new Error(
562
+ `[augment-resolver] Multiple visitorAuth augments declared (${vaCount}). visitorAuth is supported as a single instance per agent — both would attempt to register GET/POST /visitor-auth/verify routes (rejected by route-collector) and only the first's revocation state would be visible to webTransport. Declare exactly one visitorAuth augment.`,
563
+ );
564
+ }
565
+
566
+ // Fix H3: cross-augment validation — visitorAuth.agentBinding MUST match
567
+ // webTransport.visitorTokens.agentBinding when both are configured. A mismatch
568
+ // silently strands visitors: the magic-link flow succeeds, but the next request
569
+ // rejects the minted token because the agentBinding field won't match.
570
+ const vaConfig = configs.find((c) => c.type === "visitorAuth");
571
+ const wtConfig = configs.find((c) => c.type === "webTransport");
572
+ if (vaConfig && wtConfig) {
573
+ const vaBinding = (vaConfig.options as Record<string, unknown> | undefined)?.agentBinding as
574
+ | string
575
+ | undefined;
576
+ const wtBinding = (
577
+ (wtConfig.options as Record<string, unknown> | undefined)?.visitorTokens as
578
+ | Record<string, unknown>
579
+ | undefined
580
+ )?.agentBinding as string | undefined;
581
+ if (vaBinding !== wtBinding) {
582
+ throw new Error(
583
+ `Cross-augment config mismatch: visitorAuth.agentBinding (${vaBinding ?? "unset"}) ` +
584
+ `must match webTransport.visitorTokens.agentBinding (${wtBinding ?? "unset"}). ` +
585
+ `Set them both to the same value (e.g., \${AUGGY_AGENT_ID}) in agent.yaml.`,
586
+ );
587
+ }
588
+ }
589
+
590
+ // Boot-time validation: warn (not error) for any tool-providing augment
591
+ // whose bundled skill is not mounted at <agent-dir>/skills/<folder>/SKILL.md.
592
+ // Per ADR-025 Decision 5 + spec §H. Runs after every factory has produced
593
+ // its tool surface so the discriminator (`tools.length > 0`) is final.
594
+ validateBundledSkills(configs, augments, agentDir);
595
+
596
+ return augments;
597
+ }