melchizedek-agents 0.16.2 → 0.18.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 (100) hide show
  1. package/.env.example +40 -3
  2. package/AGENT_SETUP.md +4 -2
  3. package/CHANGELOG.md +246 -0
  4. package/DOCUMENTATION.md +311 -15
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +125 -1
  14. package/db/hardening.sql +4 -3
  15. package/db/migrations/0011_erase_expired.sql +198 -0
  16. package/db/migrations/0012_audit_log.sql +98 -0
  17. package/dist/lib/a2a/app.d.ts +27 -0
  18. package/dist/lib/a2a/app.js +101 -5
  19. package/dist/lib/a2a/executor.d.ts +34 -4
  20. package/dist/lib/a2a/executor.js +109 -9
  21. package/dist/lib/compile.d.ts +41 -3
  22. package/dist/lib/compile.js +162 -17
  23. package/dist/lib/config.d.ts +24 -0
  24. package/dist/lib/config.js +24 -0
  25. package/dist/lib/doctor.js +17 -4
  26. package/dist/lib/index.d.ts +2 -0
  27. package/dist/lib/index.js +1 -0
  28. package/dist/lib/loadEnv.d.ts +5 -0
  29. package/dist/lib/loadEnv.js +7 -0
  30. package/dist/lib/loadSyndicate.d.ts +50 -0
  31. package/dist/lib/loadSyndicate.js +19 -3
  32. package/dist/lib/memory/erase.d.ts +5 -0
  33. package/dist/lib/memory/erase.js +5 -0
  34. package/dist/lib/memory/providers.d.ts +27 -0
  35. package/dist/lib/memory/providers.js +28 -0
  36. package/dist/lib/models/capabilities.js +10 -0
  37. package/dist/lib/models/endpoints.d.ts +1 -1
  38. package/dist/lib/models/endpoints.js +4 -1
  39. package/dist/lib/models/fallback.d.ts +41 -0
  40. package/dist/lib/models/fallback.js +105 -0
  41. package/dist/lib/models/gateway.d.ts +2 -2
  42. package/dist/lib/models/gateway.js +4 -4
  43. package/dist/lib/models/kimiLlm.d.ts +123 -0
  44. package/dist/lib/models/kimiLlm.js +171 -0
  45. package/dist/lib/models/providerMap.d.ts +2 -2
  46. package/dist/lib/models/providerMap.js +4 -1
  47. package/dist/lib/models/registry.d.ts +1 -1
  48. package/dist/lib/models/registry.js +11 -3
  49. package/dist/lib/net/redirects.d.ts +14 -0
  50. package/dist/lib/net/redirects.js +94 -0
  51. package/dist/lib/observability/audit.d.ts +21 -0
  52. package/dist/lib/observability/audit.js +39 -0
  53. package/dist/lib/observability/metrics.d.ts +6 -0
  54. package/dist/lib/observability/tracer.d.ts +14 -1
  55. package/dist/lib/observability/tracer.js +16 -2
  56. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  57. package/dist/lib/persistence/supabaseProvider.js +2 -31
  58. package/dist/lib/runtime/questions.d.ts +55 -0
  59. package/dist/lib/runtime/questions.js +107 -0
  60. package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
  61. package/dist/lib/runtime/syndicateTurn.js +165 -17
  62. package/dist/lib/storage/postgres/index.d.ts +9 -0
  63. package/dist/lib/storage/postgres/index.js +13 -0
  64. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  65. package/dist/lib/storage/rlsStatus.js +69 -0
  66. package/dist/lib/syndicateSchema.d.ts +177 -0
  67. package/dist/lib/syndicateSchema.js +343 -3
  68. package/dist/lib/toolRegistry.js +9 -0
  69. package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
  70. package/dist/lib/tools/mcpToolFactory.js +61 -4
  71. package/dist/lib/tools/openapiTools.d.ts +52 -0
  72. package/dist/lib/tools/openapiTools.js +225 -0
  73. package/dist/lib/tools/skillToolset.d.ts +135 -0
  74. package/dist/lib/tools/skillToolset.js +247 -0
  75. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  76. package/dist/lib/tools/urlContextTool.js +39 -0
  77. package/dist/lib/workflow.d.ts +72 -0
  78. package/dist/lib/workflow.js +180 -0
  79. package/dist/lib/workflowConfig.d.ts +95 -0
  80. package/dist/lib/workflowConfig.js +93 -0
  81. package/dist/scripts/a2a_server.js +51 -8
  82. package/dist/scripts/db.d.ts +9 -0
  83. package/dist/scripts/db.js +30 -15
  84. package/dist/scripts/syndicate_chat.js +39 -5
  85. package/package.json +6 -3
  86. package/skills/briefs/_shared.md +1 -1
  87. package/skills/briefs/melchizedek-author.md +8 -4
  88. package/skills/briefs/melchizedek-memory.md +1 -1
  89. package/skills/briefs/melchizedek-models.md +1 -1
  90. package/skills/briefs/melchizedek-scribe.md +4 -4
  91. package/skills/briefs/melchizedek-serve.md +1 -1
  92. package/skills/briefs/melchizedek.md +4 -1
  93. package/skills/melchizedek/SKILL.md +4 -1
  94. package/skills/melchizedek-author/SKILL.md +7 -3
  95. package/skills/melchizedek-memory/SKILL.md +1 -1
  96. package/skills/melchizedek-models/SKILL.md +1 -1
  97. package/skills/melchizedek-scribe/SKILL.md +3 -3
  98. package/skills/melchizedek-serve/SKILL.md +1 -1
  99. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  100. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * lib/tools/openapiTools.ts — any HTTP API with an OpenAPI spec becomes an
3
+ * agent's tools, from YAML.
4
+ *
5
+ * WHY: "Bring your own API" was a code task: write a `defineTool` per
6
+ * endpoint, register it, name it in YAML. ADK 2.2 ships `OpenAPIToolset`,
7
+ * which turns an OpenAPI 3 spec into one tool per operation (parameters and
8
+ * request body as the tool's schema, the operation's summary as its
9
+ * description). An agent's `openapi:` list hands it a spec file:
10
+ *
11
+ * openapi:
12
+ * - spec: "specs/weather.yaml" # beside this YAML file
13
+ * operations: [getForecast] # omitted: the GET operations only
14
+ * auth: { api_key: { env: "WEATHER_KEY", in: "header", name: "X-Api-Key" } }
15
+ *
16
+ * WHAT THE ENGINE ADDS to ADK's toolset, all of it exposure discipline:
17
+ * - READ-ONLY BY DEFAULT. Without `operations`, only GET operations become
18
+ * tools; anything that writes is exposed only by naming it, and a named
19
+ * operation can be listed under `require_approval` like a registry tool
20
+ * (ADR 0028), so a POST or a DELETE can wait for a person.
21
+ * - SECRETS FROM THE ENVIRONMENT. `auth` names an environment variable,
22
+ * never a value; a variable that is not set fails the compile, so an
23
+ * agent never runs half-authenticated. A YAML (possibly registry-stored)
24
+ * chooses both the variable and the host, so it may never name one of
25
+ * the framework's own secrets (credentialEnvProblem): the database URL,
26
+ * a provider key or an A2A secret cannot be sent to an API.
27
+ * OPENAPI_CREDENTIAL_ENVS, when the operator sets it, is the exact list
28
+ * of variables an `auth` may name.
29
+ * - THE SSRF GUARD. Every server URL the tools will call must be http(s)
30
+ * and pass lib/net/addressGuard.ts: its literal rules at compile time
31
+ * (offline), and the full check with DNS before each call (a name can
32
+ * re-resolve between calls). ALLOW_PRIVATE_OPENAPI=true permits
33
+ * private hosts for local development, as ALLOW_PRIVATE_MCP does for MCP.
34
+ * The model chooses arguments, never the host: path parameters are
35
+ * URL-encoded by ADK and dot segments refused.
36
+ * - REDIRECTS HELD TO THE SAME GUARD. ADK calls globalThis.fetch, which
37
+ * follows redirects; a call runs under lib/net/redirects.ts instead, so
38
+ * each hop is checked before it is fetched. A hop on the same origin
39
+ * gets the server's own check; a hop to another origin must pass the
40
+ * full guard even with ALLOW_PRIVATE_OPENAPI (that permits the server
41
+ * you configured, not wherever it points), and loses every header but
42
+ * content negotiation, credentials included.
43
+ * - BOUNDED RESULTS. A response larger than MAX_RESULT_CHARS is cut and
44
+ * says so; a network failure comes back as `{ error }`, never a throw.
45
+ *
46
+ * Spec files only, never URLs: a spec fetched at compile time would be a
47
+ * second outbound surface, and a spec that changes under a running agent
48
+ * changes its tools. Save the spec beside the YAML and review it like code.
49
+ */
50
+ import { existsSync, readFileSync } from 'node:fs';
51
+ import { extname, resolve } from 'node:path';
52
+ import { OpenAPIToolset, tokenToSchemeCredential } from '@google/adk';
53
+ import { blockedHostReason, checkHost } from '../net/addressGuard.js';
54
+ import { withRedirectGuard } from '../net/redirects.js';
55
+ /** Characters a single API result may carry into the conversation. */
56
+ export const MAX_RESULT_CHARS = 20_000;
57
+ /** Marks a tool built here, so require_approval may gate it (lib/compile.ts). */
58
+ export const OPENAPI_TOOL = Symbol.for('melchizedek.openapiTool');
59
+ /** snake_case, the way ADK names a tool from an operationId. */
60
+ export function toSnake(name) {
61
+ return name
62
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
63
+ .replace(/[^A-Za-z0-9]+/g, '_')
64
+ .replace(/^_+|_+$/g, '')
65
+ .toLowerCase();
66
+ }
67
+ /** True when a configured operation name (an operationId or a tool name) names this tool. */
68
+ export function namesTool(configured, tool) {
69
+ return configured === tool.name || configured === tool.operation?.operationId || toSnake(configured) === tool.name;
70
+ }
71
+ /**
72
+ * Why a server may not be called, or null. At compile time only the literal
73
+ * rules apply (no connection is made, and a build must not need DNS); before
74
+ * each call the name is resolved too, which guards the connection itself.
75
+ */
76
+ async function hostProblem(baseUrl, resolve) {
77
+ let url;
78
+ try {
79
+ url = new URL(baseUrl);
80
+ }
81
+ catch {
82
+ return `'${baseUrl}' is not a URL`;
83
+ }
84
+ if (!/^https?:$/.test(url.protocol))
85
+ return `'${baseUrl}' must be http(s)`;
86
+ if (process.env.ALLOW_PRIVATE_OPENAPI === 'true')
87
+ return null;
88
+ const reason = resolve ? await checkHost(url.hostname) : blockedHostReason(url.hostname);
89
+ return reason ? `refusing ${url.hostname}: ${reason} (set ALLOW_PRIVATE_OPENAPI=true for local development)` : null;
90
+ }
91
+ /** Prefixes and names of variables the framework itself reads: never an API credential. */
92
+ const FRAMEWORK_ENV_PREFIXES = [
93
+ 'A2A_', 'SUPABASE_', 'DATABASE_', 'MCP_', 'MODEL_', 'MEMORY_', 'OTEL_', 'TELEMETRY_', 'MELCHIZEDEK_',
94
+ 'GOOGLE_', 'GEMINI_', 'ANTHROPIC_', 'OPENAI_', 'AZURE_', 'AWS_', 'XAI_', 'MOONSHOT_', 'OLLAMA_', 'WIKI_',
95
+ 'ALLOW_', 'OPENAPI_',
96
+ ];
97
+ const FRAMEWORK_ENV_NAMES = new Set(['PUBLIC_URL', 'HOST', 'PORT', 'PATH', 'HOME', 'NODE_OPTIONS', 'WEB_EXTRACT_CHAR_LIMIT']);
98
+ /**
99
+ * Why an `auth` may not read this variable, or null. With
100
+ * OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
101
+ * it, anything but the framework's own variables is.
102
+ */
103
+ export function credentialEnvProblem(name, env = process.env) {
104
+ const allow = env.OPENAPI_CREDENTIAL_ENVS?.split(',').map((s) => s.trim()).filter(Boolean);
105
+ if (allow?.length) {
106
+ return allow.includes(name) ? null : `${name} is not in OPENAPI_CREDENTIAL_ENVS (${allow.join(', ')})`;
107
+ }
108
+ if (FRAMEWORK_ENV_NAMES.has(name) || FRAMEWORK_ENV_PREFIXES.some((p) => name.startsWith(p))) {
109
+ return `${name} is one of the framework's own settings and may not be sent to an API; give the API its own variable (or list it in OPENAPI_CREDENTIAL_ENVS)`;
110
+ }
111
+ return null;
112
+ }
113
+ function credentialFor(auth, spec) {
114
+ if (!auth)
115
+ return {};
116
+ const read = (env) => {
117
+ const refused = credentialEnvProblem(env);
118
+ if (refused)
119
+ throw new Error(`openapi ${spec}: ${refused}`);
120
+ const value = process.env[env]?.trim();
121
+ if (!value)
122
+ throw new Error(`openapi ${spec}: ${env} is not set (auth reads it from the environment)`);
123
+ return value;
124
+ };
125
+ if (auth.bearer_env) {
126
+ const [authScheme, authCredential] = tokenToSchemeCredential('oauth2Token', undefined, undefined, read(auth.bearer_env));
127
+ return { authScheme, authCredential };
128
+ }
129
+ if (auth.api_key) {
130
+ const [authScheme, authCredential] = tokenToSchemeCredential('apikey', auth.api_key.in, auth.api_key.name, read(auth.api_key.env));
131
+ return { authScheme, authCredential };
132
+ }
133
+ return {};
134
+ }
135
+ /**
136
+ * Where an API call may be redirected: the server's own rule on its origin,
137
+ * the full guard (no development exception) anywhere else.
138
+ */
139
+ const REDIRECTS = {
140
+ async hopProblem(url, crossOrigin) {
141
+ if (!/^https?:$/.test(url.protocol))
142
+ return `${url.protocol} is not http(s)`;
143
+ if (!crossOrigin)
144
+ return hostProblem(url.href, true);
145
+ const reason = await checkHost(url.hostname);
146
+ return reason ? `refusing ${url.hostname}: ${reason}` : null;
147
+ },
148
+ };
149
+ /** Cut an API result to MAX_RESULT_CHARS, saying so. */
150
+ export function boundResult(result) {
151
+ const text = typeof result === 'string' ? result : JSON.stringify(result ?? null);
152
+ if (text.length <= MAX_RESULT_CHARS)
153
+ return result;
154
+ return { truncated: true, text: `${text.slice(0, MAX_RESULT_CHARS)}… [cut at ${MAX_RESULT_CHARS} characters]` };
155
+ }
156
+ /**
157
+ * The tools one `openapi:` entry gives an agent. Throws (failing the compile)
158
+ * on a missing spec, an unset auth variable, an operation the spec does not
159
+ * have, or a server the guard refuses.
160
+ */
161
+ export async function buildOpenApiTools(entry, baseDir = process.cwd()) {
162
+ const specPath = resolve(baseDir, entry.spec);
163
+ if (!existsSync(specPath))
164
+ throw new Error(`openapi: spec not found: ${entry.spec}`);
165
+ const ext = extname(specPath).toLowerCase();
166
+ if (!['.yaml', '.yml', '.json'].includes(ext))
167
+ throw new Error(`openapi ${entry.spec}: a spec is .yaml, .yml or .json`);
168
+ const toolset = new OpenAPIToolset({
169
+ specStr: readFileSync(specPath, 'utf-8'),
170
+ specType: ext === '.json' ? 'json' : 'yaml',
171
+ ...(entry.prefix ? { prefix: entry.prefix } : {}),
172
+ ...credentialFor(entry.auth, entry.spec),
173
+ });
174
+ const all = (await toolset.getTools());
175
+ let chosen;
176
+ if (entry.operations?.length) {
177
+ const unknown = entry.operations.filter((op) => !all.some((t) => namesTool(op, t) || namesTool(op, { name: t.name.replace(`${entry.prefix}_`, ''), operation: t.operation })));
178
+ if (unknown.length) {
179
+ throw new Error(`openapi ${entry.spec}: no operation ${unknown.map((u) => `'${u}'`).join(', ')} (the spec has ${all.map((t) => t.operation?.operationId ?? t.name).join(', ')})`);
180
+ }
181
+ chosen = all.filter((t) => entry.operations.some((op) => namesTool(op, t) || namesTool(op, { name: t.name.replace(`${entry.prefix}_`, ''), operation: t.operation })));
182
+ }
183
+ else {
184
+ chosen = all.filter((t) => t.endpoint.method.toLowerCase() === 'get');
185
+ }
186
+ if (entry.base_url)
187
+ for (const t of chosen)
188
+ t.endpoint.baseUrl = entry.base_url;
189
+ for (const baseUrl of new Set(chosen.map((t) => t.endpoint.baseUrl))) {
190
+ if (!baseUrl)
191
+ throw new Error(`openapi ${entry.spec}: the spec names no server; set base_url`);
192
+ const problem = await hostProblem(baseUrl, false);
193
+ if (problem)
194
+ throw new Error(`openapi ${entry.spec}: ${problem}`);
195
+ }
196
+ return chosen.map(guarded);
197
+ }
198
+ /**
199
+ * A copy of a generated tool whose call re-checks its host, follows
200
+ * redirects only where the guard allows, catches a network failure, and
201
+ * bounds the result. The original is untouched.
202
+ */
203
+ function guarded(tool) {
204
+ const copy = Object.create(tool);
205
+ const original = tool.runAsync.bind(tool);
206
+ Object.defineProperty(copy, OPENAPI_TOOL, { value: true });
207
+ Object.defineProperty(copy, 'runAsync', {
208
+ value: async (request) => {
209
+ const problem = await hostProblem(tool.endpoint.baseUrl, true);
210
+ if (problem)
211
+ return { error: `${tool.name} was not called: ${problem}` };
212
+ try {
213
+ return boundResult(await withRedirectGuard(REDIRECTS, () => original(request)));
214
+ }
215
+ catch (err) {
216
+ return { error: `${tool.name} failed: ${err instanceof Error ? err.message : String(err)}` };
217
+ }
218
+ },
219
+ });
220
+ return copy;
221
+ }
222
+ /** True for a tool built from an `openapi:` entry. */
223
+ export function isOpenApiTool(tool) {
224
+ return !!tool && typeof tool === 'object' && tool[OPENAPI_TOOL] === true;
225
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * lib/tools/skillToolset.ts — Agent Skills for a syndicate agent, the way a
3
+ * coding harness holds them.
4
+ *
5
+ * WHY this file exists:
6
+ * A coding harness (Claude Code, Codex, Gemini CLI) keeps every installed
7
+ * skill's frontmatter in its system prompt, reads a SKILL.md in full only
8
+ * when a request matches its description, opens the files a skill ships
9
+ * only when the procedure reaches them, and runs the scripts a skill
10
+ * ships. ADK 2.2 carries the mechanism (`SkillToolset`, `loadAllSkillsInDir`,
11
+ * the open SKILL.md standard's layout: SKILL.md + references/, assets/,
12
+ * scripts/). Three things it does not do, this file does:
13
+ *
14
+ * 1. INJECT THE INDEX. ADK's TypeScript port never calls the toolset's own
15
+ * request hook, so `<available_skills>` would reach the model only
16
+ * through a `list_skills` call. `skillsInstruction()` renders the index
17
+ * once at compile time and lib/compile.ts appends it to the agent's
18
+ * instruction: no round trip, always in view.
19
+ * 2. KEEP DISCLOSURE PROGRESSIVE. ADK's `load_skill` returns every
20
+ * resource's full content with the instructions. `LeanLoadSkillTool`
21
+ * returns the instructions and the resources' NAMES; `load_skill_resource`
22
+ * reads one when the procedure asks.
23
+ * 3. GATE EVERY SCRIPT RUN. ADK's `run_skill_script` runs when called.
24
+ * `GatedRunSkillScriptTool` asks for a person's approval first through
25
+ * the same confirmation interrupt `require_approval` uses (ADR 0028),
26
+ * so the turn pauses `input-required` and resumes on approve or reject.
27
+ *
28
+ * `allowed-tools` in a skill's frontmatter is honoured through ADK's
29
+ * activation mechanism: once the skill is loaded, the tools it names
30
+ * become callable, provided the agent's YAML listed them under
31
+ * `skills.tools`. Exposure stays two deliberate acts (the skill names the
32
+ * tool, the YAML permits it); a skill cannot grant itself a tool the YAML
33
+ * withheld.
34
+ *
35
+ * WHAT RUNS: only scripts shipped in a skill's `scripts/` directory, through
36
+ * ADK's local executor (`skills.scripts: local`), after approval. The
37
+ * model's own code never executes: the executor is handed to the toolset,
38
+ * never to the agent, so ADK's code-block execution stays off. ADK's
39
+ * container executor cannot stage a skill's files yet, so it is not offered.
40
+ *
41
+ * FAILURE CONTRACT: a missing skills directory fails the compile (an agent
42
+ * running without the skills it was declared with would be a silent
43
+ * defect). Inside a turn, the ADK tools return `{ error }` objects the
44
+ * model can read, never throw.
45
+ */
46
+ import type { FunctionDeclaration } from '@google/genai';
47
+ import { BaseTool, RunSkillScriptTool, SkillToolset } from '@google/adk';
48
+ import type { ReadonlyContext, RunAsyncToolRequest } from '@google/adk';
49
+ /** What an agent's YAML `skills:` block may say. */
50
+ export interface SkillsConfig {
51
+ /** A directory of skills, one subdirectory each holding a SKILL.md. Relative to the working directory. */
52
+ dir: string;
53
+ /** Whether a skill's `scripts/` may run. `local` runs them on this host, each after a person approves. */
54
+ scripts?: 'none' | 'local';
55
+ /** Registry tool names a skill may unlock through its `allowed-tools` frontmatter, once loaded. */
56
+ tools?: string[];
57
+ }
58
+ /** ADK's Skill, as this module reads it (the type is not exported from the package index). */
59
+ export interface LoadedSkill {
60
+ frontmatter: {
61
+ name: string;
62
+ description: string;
63
+ allowedTools?: string;
64
+ metadata?: Record<string, unknown>;
65
+ [key: string]: unknown;
66
+ };
67
+ instructions: string;
68
+ resources?: {
69
+ references?: Record<string, unknown>;
70
+ assets?: Record<string, unknown>;
71
+ scripts?: Record<string, unknown>;
72
+ };
73
+ }
74
+ /** Seconds a skill script may run before the local executor kills it. */
75
+ export declare const SCRIPT_TIMEOUT_SECONDS = 120;
76
+ /**
77
+ * Load every skill under `dir` (ADK's loader: a skill is a directory whose
78
+ * name equals its frontmatter `name`, holding a SKILL.md with a description;
79
+ * an invalid skill is skipped with ADK's warning). A directory that does
80
+ * not exist is an error: the YAML declared skills this agent cannot have.
81
+ */
82
+ export declare function loadSkillSuite(dir: string): Promise<Record<string, LoadedSkill>>;
83
+ /**
84
+ * Why a skill directory under `dir` was NOT loaded: ADK skips an invalid
85
+ * skill with a log line the operator may never see, so the compiler reports
86
+ * each one. A candidate is a subdirectory holding a SKILL.md.
87
+ */
88
+ export declare function skillSuiteProblems(dir: string): Promise<string[]>;
89
+ /**
90
+ * The block lib/compile.ts appends to an agent's instruction: the frontmatter
91
+ * index every request sees, and the names of the tools that read and run a
92
+ * skill. The ORDER of use (read before following, cite, when to run a
93
+ * script) is the YAML instruction's to state; this block only says what
94
+ * exists.
95
+ */
96
+ export declare function skillsInstruction(skills: Record<string, LoadedSkill>, config: SkillsConfig): string;
97
+ /**
98
+ * ADK's `load_skill`, minus the resource dump: the procedure in full, the
99
+ * frontmatter, and the names of the files the skill ships. Loading also
100
+ * activates the skill for the agent (ADK's state key), which is what unlocks
101
+ * its `allowed-tools`.
102
+ */
103
+ export declare class LeanLoadSkillTool extends BaseTool {
104
+ private readonly toolset;
105
+ constructor(toolset: SkillToolset);
106
+ _getDeclaration(): FunctionDeclaration;
107
+ runAsync({ args, toolContext }: RunAsyncToolRequest): Promise<unknown>;
108
+ }
109
+ /**
110
+ * ADK's `run_skill_script` behind a person's approval: the first call raises
111
+ * the confirmation interrupt (the hint names the skill, the script and the
112
+ * arguments) and returns without running; the resumed call runs only when
113
+ * the answer confirmed it. Mirrors FunctionTool's own gate, so the turn
114
+ * runner and the A2A server treat it exactly as `require_approval` (ADR 0028).
115
+ */
116
+ export declare class GatedRunSkillScriptTool extends RunSkillScriptTool {
117
+ runAsync(request: RunAsyncToolRequest): Promise<unknown>;
118
+ }
119
+ /**
120
+ * The toolset an agent with `skills:` carries. `list_skills` is left out
121
+ * (the index is in the instruction); `load_skill` is the lean one;
122
+ * `run_skill_script` is present, gated, only when scripts may run.
123
+ */
124
+ export declare class HarnessSkillToolset extends SkillToolset {
125
+ private readonly harnessTools;
126
+ constructor(skills: Record<string, LoadedSkill>, config: SkillsConfig, additionalTools?: unknown[]);
127
+ getTools(context?: ReadonlyContext): Promise<BaseTool[]>;
128
+ }
129
+ /** Build the toolset and its instruction block for one agent. */
130
+ export declare function buildSkillHarness(config: SkillsConfig, additionalTools: unknown[]): Promise<{
131
+ toolset: HarnessSkillToolset;
132
+ instruction: string;
133
+ skills: Record<string, LoadedSkill>;
134
+ problems: string[];
135
+ }>;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * lib/tools/skillToolset.ts — Agent Skills for a syndicate agent, the way a
3
+ * coding harness holds them.
4
+ *
5
+ * WHY this file exists:
6
+ * A coding harness (Claude Code, Codex, Gemini CLI) keeps every installed
7
+ * skill's frontmatter in its system prompt, reads a SKILL.md in full only
8
+ * when a request matches its description, opens the files a skill ships
9
+ * only when the procedure reaches them, and runs the scripts a skill
10
+ * ships. ADK 2.2 carries the mechanism (`SkillToolset`, `loadAllSkillsInDir`,
11
+ * the open SKILL.md standard's layout: SKILL.md + references/, assets/,
12
+ * scripts/). Three things it does not do, this file does:
13
+ *
14
+ * 1. INJECT THE INDEX. ADK's TypeScript port never calls the toolset's own
15
+ * request hook, so `<available_skills>` would reach the model only
16
+ * through a `list_skills` call. `skillsInstruction()` renders the index
17
+ * once at compile time and lib/compile.ts appends it to the agent's
18
+ * instruction: no round trip, always in view.
19
+ * 2. KEEP DISCLOSURE PROGRESSIVE. ADK's `load_skill` returns every
20
+ * resource's full content with the instructions. `LeanLoadSkillTool`
21
+ * returns the instructions and the resources' NAMES; `load_skill_resource`
22
+ * reads one when the procedure asks.
23
+ * 3. GATE EVERY SCRIPT RUN. ADK's `run_skill_script` runs when called.
24
+ * `GatedRunSkillScriptTool` asks for a person's approval first through
25
+ * the same confirmation interrupt `require_approval` uses (ADR 0028),
26
+ * so the turn pauses `input-required` and resumes on approve or reject.
27
+ *
28
+ * `allowed-tools` in a skill's frontmatter is honoured through ADK's
29
+ * activation mechanism: once the skill is loaded, the tools it names
30
+ * become callable, provided the agent's YAML listed them under
31
+ * `skills.tools`. Exposure stays two deliberate acts (the skill names the
32
+ * tool, the YAML permits it); a skill cannot grant itself a tool the YAML
33
+ * withheld.
34
+ *
35
+ * WHAT RUNS: only scripts shipped in a skill's `scripts/` directory, through
36
+ * ADK's local executor (`skills.scripts: local`), after approval. The
37
+ * model's own code never executes: the executor is handed to the toolset,
38
+ * never to the agent, so ADK's code-block execution stays off. ADK's
39
+ * container executor cannot stage a skill's files yet, so it is not offered.
40
+ *
41
+ * FAILURE CONTRACT: a missing skills directory fails the compile (an agent
42
+ * running without the skills it was declared with would be a silent
43
+ * defect). Inside a turn, the ADK tools return `{ error }` objects the
44
+ * model can read, never throw.
45
+ */
46
+ import { readdirSync, realpathSync } from 'node:fs';
47
+ import { resolve } from 'node:path';
48
+ import { Type } from '@google/genai';
49
+ import { BaseTool, LoadSkillResourceTool, RunSkillScriptTool, SkillToolset, UnsafeLocalCodeExecutor, loadAllSkillsInDir, validateSkillDir, } from '@google/adk';
50
+ /** Seconds a skill script may run before the local executor kills it. */
51
+ export const SCRIPT_TIMEOUT_SECONDS = 120;
52
+ /**
53
+ * Load every skill under `dir` (ADK's loader: a skill is a directory whose
54
+ * name equals its frontmatter `name`, holding a SKILL.md with a description;
55
+ * an invalid skill is skipped with ADK's warning). A directory that does
56
+ * not exist is an error: the YAML declared skills this agent cannot have.
57
+ */
58
+ export async function loadSkillSuite(dir) {
59
+ const abs = resolve(dir);
60
+ let real;
61
+ try {
62
+ real = realpathSync(abs);
63
+ readdirSync(real);
64
+ }
65
+ catch {
66
+ throw new Error(`skills: directory not found or unreadable: ${dir}`);
67
+ }
68
+ const skills = (await loadAllSkillsInDir(real));
69
+ for (const skill of Object.values(skills))
70
+ honourAllowedTools(skill);
71
+ return skills;
72
+ }
73
+ /**
74
+ * Why a skill directory under `dir` was NOT loaded: ADK skips an invalid
75
+ * skill with a log line the operator may never see, so the compiler reports
76
+ * each one. A candidate is a subdirectory holding a SKILL.md.
77
+ */
78
+ export async function skillSuiteProblems(dir) {
79
+ const real = realpathSync(resolve(dir));
80
+ const out = [];
81
+ for (const entry of readdirSync(real, { withFileTypes: true })) {
82
+ if (!entry.isDirectory())
83
+ continue;
84
+ const skillDir = resolve(real, entry.name);
85
+ if (!readdirSync(skillDir).some((f) => f.toLowerCase() === 'skill.md'))
86
+ continue;
87
+ for (const problem of await validateSkillDir(skillDir))
88
+ out.push(`${entry.name}: ${problem.split('\n')[0]}`);
89
+ }
90
+ return out;
91
+ }
92
+ /**
93
+ * The open standard's `allowed-tools` (a space- or comma-separated list of
94
+ * tool names) becomes ADK's `metadata.adk_additional_tools`, which is what
95
+ * the toolset reads when the skill is loaded. A skill that already set the
96
+ * ADK field keeps it.
97
+ */
98
+ function honourAllowedTools(skill) {
99
+ const fm = skill.frontmatter;
100
+ const declared = typeof fm.allowedTools === 'string' ? fm.allowedTools : typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'] : '';
101
+ const names = declared.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
102
+ if (names.length === 0)
103
+ return;
104
+ fm.metadata = { ...(fm.metadata ?? {}) };
105
+ if (!Array.isArray(fm.metadata.adk_additional_tools))
106
+ fm.metadata.adk_additional_tools = names;
107
+ }
108
+ function escapeXml(s) {
109
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
110
+ }
111
+ /**
112
+ * The block lib/compile.ts appends to an agent's instruction: the frontmatter
113
+ * index every request sees, and the names of the tools that read and run a
114
+ * skill. The ORDER of use (read before following, cite, when to run a
115
+ * script) is the YAML instruction's to state; this block only says what
116
+ * exists.
117
+ */
118
+ export function skillsInstruction(skills, config) {
119
+ const entries = Object.values(skills).sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
120
+ const index = entries.length === 0
121
+ ? '<available_skills>\n</available_skills>'
122
+ : [
123
+ '<available_skills>',
124
+ ...entries.flatMap((s) => [
125
+ ' <skill>',
126
+ ` <name>${escapeXml(s.frontmatter.name)}</name>`,
127
+ ` <description>${escapeXml(s.frontmatter.description.trim())}</description>`,
128
+ ' </skill>',
129
+ ]),
130
+ '</available_skills>',
131
+ ].join('\n');
132
+ const scripts = config.scripts === 'local';
133
+ const lines = [
134
+ 'Skills are installed procedures, one directory each: a SKILL.md with the procedure, and optionally references/, assets/ and scripts/. The index below lists every installed skill by name and description; it is complete, so never call a tool to list skills.',
135
+ 'load_skill(name) returns a skill\'s full SKILL.md and the names of the files it ships. load_skill_resource(skill_name, path) returns one of those files ("references/x.md", "assets/x.yaml", "scripts/x.sh").',
136
+ scripts
137
+ ? 'run_skill_script(skill_name, script_path, args) runs a script from a skill\'s scripts/ directory on this machine. Every run waits for the user\'s approval before it executes; when a run is refused, say so and do not retry it.'
138
+ : 'Scripts in a skill\'s scripts/ directory cannot run here; when a procedure says to run one, give the user the exact command instead.',
139
+ index,
140
+ ];
141
+ return lines.join('\n\n');
142
+ }
143
+ function resourceNames(skill) {
144
+ const out = [];
145
+ for (const [dir, files] of Object.entries(skill.resources ?? {})) {
146
+ for (const name of Object.keys(files ?? {}))
147
+ out.push(`${dir}/${name}`);
148
+ }
149
+ return out.sort();
150
+ }
151
+ /**
152
+ * ADK's `load_skill`, minus the resource dump: the procedure in full, the
153
+ * frontmatter, and the names of the files the skill ships. Loading also
154
+ * activates the skill for the agent (ADK's state key), which is what unlocks
155
+ * its `allowed-tools`.
156
+ */
157
+ export class LeanLoadSkillTool extends BaseTool {
158
+ toolset;
159
+ constructor(toolset) {
160
+ super({
161
+ name: 'load_skill',
162
+ description: 'Read one installed skill in full: its SKILL.md procedure, its frontmatter, and the names of the files it ships. Read a skill before you follow it.',
163
+ });
164
+ this.toolset = toolset;
165
+ }
166
+ _getDeclaration() {
167
+ return {
168
+ name: this.name,
169
+ description: this.description,
170
+ parameters: {
171
+ type: Type.OBJECT,
172
+ properties: { name: { type: Type.STRING, description: 'The skill\'s name, exactly as the index lists it.' } },
173
+ required: ['name'],
174
+ },
175
+ };
176
+ }
177
+ async runAsync({ args, toolContext }) {
178
+ const name = typeof args.name === 'string' ? args.name.trim() : '';
179
+ if (!name)
180
+ return { error: 'Skill name is required.', error_code: 'MISSING_SKILL_NAME' };
181
+ const skill = this.toolset.getSkill(name);
182
+ if (!skill) {
183
+ return { error: `Skill '${name}' not found. Installed: ${Object.keys(this.toolset.skills).sort().join(', ') || '(none)'}.`, error_code: 'SKILL_NOT_FOUND' };
184
+ }
185
+ // The same state key ADK's own load_skill sets: SkillToolset reads it to
186
+ // resolve the skill's additional tools on the next request.
187
+ const agentName = toolContext.invocationContext.agent?.name ?? toolContext.agentName;
188
+ const stateKey = `_adk_activated_skill_${agentName}`;
189
+ const activated = toolContext.state.get(stateKey) ?? [];
190
+ if (!activated.includes(name))
191
+ toolContext.state.set(stateKey, [...activated, name]);
192
+ return { skill_name: name, frontmatter: skill.frontmatter, instructions: skill.instructions, files: resourceNames(skill) };
193
+ }
194
+ }
195
+ /**
196
+ * ADK's `run_skill_script` behind a person's approval: the first call raises
197
+ * the confirmation interrupt (the hint names the skill, the script and the
198
+ * arguments) and returns without running; the resumed call runs only when
199
+ * the answer confirmed it. Mirrors FunctionTool's own gate, so the turn
200
+ * runner and the A2A server treat it exactly as `require_approval` (ADR 0028).
201
+ */
202
+ export class GatedRunSkillScriptTool extends RunSkillScriptTool {
203
+ async runAsync(request) {
204
+ const { args, toolContext } = request;
205
+ const ctx = toolContext;
206
+ if (!ctx.toolConfirmation) {
207
+ const summary = `${String(args.skill_name ?? '?')}: ${String(args.script_path ?? '?')}${args.args ? ` ${JSON.stringify(args.args)}` : ''}`;
208
+ ctx.requestConfirmation({
209
+ hint: `Approval is required before a skill script runs on this machine. The agent asks to run ${summary}.`,
210
+ payload: { skill_name: args.skill_name, script_path: args.script_path, args: args.args ?? {} },
211
+ });
212
+ ctx.actions.skipSummarization = true;
213
+ return { error: 'This script run requires approval, please approve or reject.' };
214
+ }
215
+ if (!ctx.toolConfirmation.confirmed)
216
+ return { error: 'This script run was rejected.' };
217
+ return super.runAsync(request);
218
+ }
219
+ }
220
+ /**
221
+ * The toolset an agent with `skills:` carries. `list_skills` is left out
222
+ * (the index is in the instruction); `load_skill` is the lean one;
223
+ * `run_skill_script` is present, gated, only when scripts may run.
224
+ */
225
+ export class HarnessSkillToolset extends SkillToolset {
226
+ harnessTools;
227
+ constructor(skills, config, additionalTools = []) {
228
+ const codeExecutor = config.scripts === 'local' ? new UnsafeLocalCodeExecutor({ timeoutSeconds: SCRIPT_TIMEOUT_SECONDS }) : undefined;
229
+ super(skills, { codeExecutor, additionalTools: additionalTools });
230
+ this.harnessTools = [new LeanLoadSkillTool(this), new LoadSkillResourceTool(this)];
231
+ if (codeExecutor)
232
+ this.harnessTools.push(new GatedRunSkillScriptTool(this));
233
+ }
234
+ async getTools(context) {
235
+ // The parent resolves the skills' additional tools from the activation
236
+ // state; its own fixed tools are replaced by the harness set.
237
+ const parentTools = await super.getTools(context);
238
+ const fixed = new Set(['list_skills', 'load_skill', 'load_skill_resource', 'run_skill_script', 'run_skill_inline_script', 'search_skills']);
239
+ return [...this.harnessTools, ...parentTools.filter((t) => !fixed.has(t.name))];
240
+ }
241
+ }
242
+ /** Build the toolset and its instruction block for one agent. */
243
+ export async function buildSkillHarness(config, additionalTools) {
244
+ const skills = await loadSkillSuite(config.dir);
245
+ const toolset = new HarnessSkillToolset(skills, config, additionalTools);
246
+ return { toolset, instruction: skillsInstruction(skills, config), skills, problems: await skillSuiteProblems(config.dir) };
247
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * lib/tools/urlContextTool.ts — `url_context`: the model reads the pages
3
+ * whose URLs appear in the conversation, server-side, on Gemini.
4
+ *
5
+ * WHY: ADK's URL_CONTEXT is Gemini's built-in URL reading, and it THROWS for
6
+ * any other model, which would fail a mixed-provider syndicate at request
7
+ * time. This is the same tool made safe to declare anywhere, the pattern
8
+ * web_search uses: on a Gemini model it adds `{ urlContext: {} }` to the
9
+ * request; on any other it does nothing, and `npm run doctor` reports it as
10
+ * dropped (lib/models/capabilities.ts). For reading a page on every
11
+ * provider, declare `web_extract` (client-side, SSRF-guarded).
12
+ *
13
+ * Exposure: Google fetches the URLs, not this host, so no address of this
14
+ * deployment's network is reachable through it. Page text arrives as model
15
+ * input; treat it as data, as any fetched page.
16
+ */
17
+ import { BaseTool } from '@google/adk';
18
+ import type { LlmRequest } from '@google/adk';
19
+ export declare const URL_CONTEXT_TOOL_NAME = "url_context";
20
+ export declare class UrlContextTool extends BaseTool {
21
+ constructor();
22
+ /** Never a client-side function tool. */
23
+ _getDeclaration(): undefined;
24
+ runAsync(): Promise<unknown>;
25
+ processLlmRequest({ llmRequest }: {
26
+ llmRequest: LlmRequest;
27
+ }): Promise<void>;
28
+ }
29
+ export declare const URL_CONTEXT: UrlContextTool;