Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
},
"devDependencies": {
"@intx/agent": "0.4.0",
"@intx/storage-isogit": "0.4.0",
"@intx/types": "0.4.0",
"@types/bun": "1.3.14",
"@types/node": "22.10.5",
Expand Down
62 changes: 62 additions & 0 deletions src/sidecar-bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// The deploy-time capability walk reads `gmail.definitions` without
// invoking the factory, so the static declaration must match what the
// instantiated bundle emits.

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { createDefaultDirectorRegistry } from "@intx/agent";
import { noopAuditStore, permissiveAuthorize } from "@intx/agent/testing";
import { createIsogitStore } from "@intx/storage-isogit/node";
import type { InferenceSource } from "@intx/types/runtime";
import { createRuntimeCapabilities } from "@intx/types/runtime-capabilities";

import { gmail, type GmailToolEnv } from "./sidecar-bundle.js";

const SOURCE: InferenceSource = {
id: "anthropic:mock-model",
provider: "anthropic",
baseURL: "https://api.anthropic.com",
credentialId: "test-credential",
model: "mock-model",
};

let tmpDir: string;
let env: GmailToolEnv;

beforeAll(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "google-tools-sidecar-bundle-test-"));
env = {
sources: [SOURCE],
defaultSource: SOURCE.id,
storage: await createIsogitStore(tmpDir),
workdir: tmpDir,
audit: noopAuditStore(),
authorize: permissiveAuthorize(),
directors: createDefaultDirectorRegistry(),
capabilities: createRuntimeCapabilities({}),
};
});

afterAll(async () => {
await rm(tmpDir, { recursive: true, force: true });
});

describe("gmail sidecar-bundle static declaration", () => {
test("declared definition names match the instantiated bundle's names", async () => {
const bundle = gmail(env);
const declared = new Set(gmail.definitions.map((definition) => definition.name));
const emitted = new Set(bundle.definitions.map((definition) => definition.name));
expect(emitted).toEqual(declared);
await bundle.dispose?.();
});

test("gates every tool behind approval", () => {
expect(gmail.definitions.length).toBeGreaterThan(0);
for (const definition of gmail.definitions) {
expect(definition.approval).toBe("ask");
}
});
});
36 changes: 32 additions & 4 deletions src/sidecar-bundle.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
// Sidecar-bundle entry for `@corbits/google-tools` — the convention-compliant
// factory the tool-package loader invokes.
//
// The bundle consumes the host-assembled runtime capabilities rather than
// building its own. The host (the sidecar's step-env builder) owns the
// `RuntimeCapabilities` and puts it on `env.capabilities`; this factory
// resolves the `gmail-api` credential from it through `createGmailTools`. The
// env key it touches (`capabilities`) is declared in `requires`.

import { defineTool, type BaseEnv } from "@intx/agent";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";

import { createGmailTools } from "./tools/create-tools.js";
import { GMAIL_TOOL_CATALOG } from "./tools/definitions.js";

/**
* Env contract for the Gmail tool bundle. Extends `BaseEnv` with the
* host-assembled `capabilities`, from which the Gmail tools resolve the
* `gmail-api` credential.
*/
export interface GmailToolEnv extends BaseEnv {
capabilities: RuntimeCapabilities;
}

/**
* Named export the loader picks up. The id is package-namespaced per
* the convention; the model-facing tool names are synthesized by the
* loader as `@corbits/google-tools/sidecar-bundle:<def.name>`.
*/
export const gmail = defineTool<GmailToolEnv>({
id: "@corbits/google-tools/sidecar-bundle",
requires: ["capabilities"],
definitions: GMAIL_TOOL_CATALOG.map((definition) => ({
name: definition.name,
...(definition.approval === undefined ? {} : { approval: definition.approval }),
// Unlike tools-mail, every tool declares approval: Gmail is third-party
// data, so each tool asks until a person saves an allow grant.
definitions: GMAIL_TOOL_CATALOG.map((def) => ({
name: def.name,
approval: def.approval,
})),
factory: (env) => createGmailTools({ capabilities: env.capabilities }),
factory: (env) => {
const tools = createGmailTools({ capabilities: env.capabilities });
return {
definitions: tools.definitions,
run: (call, signal) => tools.run(call, signal),
dispose: () => tools.dispose(),
};
},
});
Loading
Loading