From abb8573b55e06b8c672593b05e79112fba43c71f Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 05:20:01 -0400 Subject: [PATCH 1/9] feat(sdk,agent-registry-mongo): library-mode telemetry hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user-facing question: when ComputerAgent is just npm-installed into a customer's existing worker (Temporal pod, CLI batch, serverless fn), how do we make every run visible in the AgentOS dashboard with no extra orchestration? Answer: an AgentTelemetry interface the SDK fires on construct + each chat + dispose, plus a first-class Mongo implementation. SDK (@computeragent/sdk): - New AgentTelemetry interface (sdk/src/telemetry.ts): optional onAgentConstructed, onChatStart, onChatEnd, onClose hooks. Pure data contract — no Mongo. - ComputerAgent constructor accepts opts.telemetry, fires onAgentConstructed. - chat() fires onChatStart synchronously and attaches a .then to the handle for onChatEnd (success + error paths). Telemetry .then runs alongside the caller's await — ChatHandle.then() is memoized via result(). - dispose() fires onClose. All calls fire-and-forget (safeFireTelemetry): telemetry exceptions never propagate to the agent run. - Re-exports AgentTelemetry + Info types from index.ts. New package @computeragent/agent-registry-mongo: - AgentRegistry — Mongo wrapper for agent_registry (one doc per agent, idempotent upsert by name). register/unregister/get/list/close. - AgentLogStore — promoted from examples/agent-log-store.ts; now keyed by agentName not bot, with usage/durationMs/error fields. append/list/count. - MongoTelemetry — the headline class. Implements AgentTelemetry by writing to both collections. Single constructor takes {url, database, agent: { name, label?, harness?, source?, model?, registeredBy? }}. Optional shared MongoClient + onError callback. - README with the additive customer flow. Verified: pnpm sdk build clean, pnpm agent-registry-mongo build clean, all 45 existing SDK tests still pass. This is the foundation that makes "swap computer agent as a package on their K8s and we can still track it" actually true. --- packages/agent-registry-mongo/README.md | 124 +++++++++++ packages/agent-registry-mongo/package.json | 49 +++++ .../agent-registry-mongo/src/audit-log.ts | 154 +++++++++++++ packages/agent-registry-mongo/src/index.ts | 14 ++ packages/agent-registry-mongo/src/registry.ts | 133 +++++++++++ .../agent-registry-mongo/src/telemetry.ts | 207 ++++++++++++++++++ packages/agent-registry-mongo/tsconfig.json | 9 + packages/sdk/src/computer-agent.ts | 122 ++++++++++- packages/sdk/src/index.ts | 6 + packages/sdk/src/telemetry.ts | 72 ++++++ packages/sdk/src/types.ts | 14 ++ 11 files changed, 902 insertions(+), 2 deletions(-) create mode 100644 packages/agent-registry-mongo/README.md create mode 100644 packages/agent-registry-mongo/package.json create mode 100644 packages/agent-registry-mongo/src/audit-log.ts create mode 100644 packages/agent-registry-mongo/src/index.ts create mode 100644 packages/agent-registry-mongo/src/registry.ts create mode 100644 packages/agent-registry-mongo/src/telemetry.ts create mode 100644 packages/agent-registry-mongo/tsconfig.json create mode 100644 packages/sdk/src/telemetry.ts diff --git a/packages/agent-registry-mongo/README.md b/packages/agent-registry-mongo/README.md new file mode 100644 index 0000000..8fe9497 --- /dev/null +++ b/packages/agent-registry-mongo/README.md @@ -0,0 +1,124 @@ +# @computeragent/agent-registry-mongo + +MongoDB-backed agent registry + per-run audit log + `AgentTelemetry` implementation for `ComputerAgent`. + +Use this when ComputerAgent runs as a **library** inside an existing worker (e.g. a Temporal worker pod, a CLI batch job, a serverless function) and you want the [AgentOS dashboard](https://github.com/open-gitagent/ComputerAgent) to see every run with no extra orchestration. Pass the telemetry instance to the SDK, and the SDK fires lifecycle hooks that upsert to `agent_registry` and append to `agent_logs`. + +## Install + +```bash +npm install @computeragent/agent-registry-mongo +``` + +## Use + +```ts +import { ComputerAgent, LocalSubstrate } from "computeragent"; +import { MongoTelemetry } from "@computeragent/agent-registry-mongo"; + +const telemetry = new MongoTelemetry({ + url: process.env.MONGO_URL!, + database: "agentos", + agent: { + name: "devsupport-agent", + label: "DevSupport", + source: "github.com/nord/devsupport-agent", + harness: "claude-agent-sdk", + model: "bedrock/anthropic.claude-sonnet-4-20250514-v1:0", + registeredBy: process.env.HOSTNAME, // pod name, "ci", etc. + }, +}); + +await using agent = new ComputerAgent({ + source: {type: "git", url: "github.com/nord/devsupport-agent"}, + harness: "claude-agent-sdk", + runtime: new LocalSubstrate(), + envs: { CLAUDE_CODE_USE_BEDROCK: "1", AWS_REGION: "us-west-2" }, + telemetry, +}); + +const result = await agent.chat("hello"); +``` + +That's it. On construct, an upsert lands in `agent_registry`. On every `chat()`, a document lands in `agent_logs` with `usage`, `durationMs`, `ok`, `error`, and a truncated reply. + +## Collections + +### `agent_registry` + +```ts +{ + _id: "devsupport-agent", // agent name (unique) + label: "DevSupport", + harness: "claude-agent-sdk", + source: "github.com/nord/...", // IdentitySource JSON + model: "bedrock/anthropic.claude-sonnet-4-...", + registeredBy: "worker-abc-123", + registeredAt: ISODate("..."), + updatedAt: ISODate("..."), + lastSeen: ISODate("..."), +} +``` + +### `agent_logs` + +```ts +{ + _id: "log_", + ts: ISODate("..."), + source: "library", // | "slack" | "web" | "schedule" | ... + agentName: "devsupport-agent", + sessionId: "session_...", + query: "user message text", + reply: "assistant reply text", + ok: true, + durationMs: 12345, + inputTokens: 1200, + outputTokens: 350, + costUsd: 0.012, +} +``` + +## Lower-level API + +If you want CRUD beyond the telemetry hook (e.g. unregistering an agent from a teardown script): + +```ts +import { AgentRegistry, AgentLogStore } from "@computeragent/agent-registry-mongo"; + +const reg = new AgentRegistry({url, database: "agentos"}); +await reg.register({name: "agent-1", harness: "claude-agent-sdk", source: "..."}); +await reg.list(); +await reg.unregister("agent-1"); +await reg.close(); + +const logs = new AgentLogStore({url, database: "agentos"}); +const recent = await logs.list({agentName: "agent-1", limit: 20}); +``` + +## Connection sharing + +If your app already manages a `MongoClient`, pass it in to avoid a second pool: + +```ts +const client = new MongoClient(process.env.MONGO_URL!); +await client.connect(); +const telemetry = new MongoTelemetry({ + url: process.env.MONGO_URL!, + database: "agentos", + agent: {name: "devsupport-agent", harness: "claude-agent-sdk", source: "..."}, + client, // shared +}); +// telemetry.onClose() won't close `client` — you own its lifecycle. +``` + +## Error policy + +Telemetry calls are fire-and-forget on the SDK side — exceptions thrown here never reach the agent's chat result. For diagnostics, pass an `onError` callback: + +```ts +new MongoTelemetry({ + ..., + onError: (err, op) => myLogger.warn({err, op}, "telemetry write failed"), +}); +``` diff --git a/packages/agent-registry-mongo/package.json b/packages/agent-registry-mongo/package.json new file mode 100644 index 0000000..db8362e --- /dev/null +++ b/packages/agent-registry-mongo/package.json @@ -0,0 +1,49 @@ +{ + "name": "@computeragent/agent-registry-mongo", + "version": "0.1.0", + "description": "MongoDB-backed agent registry + per-run audit log + AgentTelemetry impl for ComputerAgent. Makes library-mode deployments (e.g. inside a Temporal worker pod) visible in the AgentOS dashboard with no extra orchestration.", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run --passWithNoTests", + "clean": "rm -rf dist .turbo *.tsbuildinfo" + }, + "dependencies": { + "@computeragent/protocol": "workspace:*", + "@computeragent/sdk": "workspace:*", + "mongodb": "^6.10.0" + }, + "devDependencies": { + "typescript": "^5.5.0", + "vitest": "^2.0.0" + }, + "keywords": [ + "ai-agents", + "agent-registry", + "audit-log", + "telemetry", + "mongodb", + "computeragent", + "agentos" + ], + "homepage": "https://github.com/open-gitagent/ComputerAgent", + "repository": { + "type": "git", + "url": "https://github.com/open-gitagent/ComputerAgent.git", + "directory": "packages/agent-registry-mongo" + } +} diff --git a/packages/agent-registry-mongo/src/audit-log.ts b/packages/agent-registry-mongo/src/audit-log.ts new file mode 100644 index 0000000..6053ec5 --- /dev/null +++ b/packages/agent-registry-mongo/src/audit-log.ts @@ -0,0 +1,154 @@ +/** + * `agent_logs` collection — one document per agent run / chat turn. + * + * Originally lived in `examples/agent-log-store.ts` (the Slack bot wrote to + * it directly). Promoted to a real package so the SDK can write to it via + * the `AgentTelemetry` hook, making library-mode runs visible in AgentOS + * with no extra plumbing. + * + * Schema: + * + * { + * _id: string, // "log_" + * ts: Date, + * source: string, // "slack" | "web" | "library" | "schedule" | "test" ... + * agentName: string, // matches agent_registry._id + * requester: string | null, // Slack user id, "library", null for schedule + * channel: string | null, + * threadTs: string | null, + * sessionId: string | null, + * query: string, // truncated to QUERY_MAX + * reply: string, // truncated to REPLY_MAX + * ok: boolean, + * error?: string, + * durationMs?: number, + * inputTokens?: number, + * outputTokens?: number, + * costUsd?: number | null, + * } + */ +import { MongoClient, type Collection } from "mongodb"; +import { randomUUID } from "node:crypto"; + +export const QUERY_MAX = 8_000; +export const REPLY_MAX = 16_000; + +export interface AgentLogEntry { + _id: string; + ts: Date; + source: string; + agentName: string; + requester: string | null; + channel: string | null; + threadTs: string | null; + sessionId: string | null; + query: string; + reply: string; + ok: boolean; + error?: string; + durationMs?: number; + inputTokens?: number; + outputTokens?: number; + costUsd?: number | null; +} + +export type NewAgentLog = Omit & { ts?: Date }; + +export interface AgentLogFilter { + agentName?: string; + source?: string; + ok?: boolean; + limit?: number; + before?: Date; +} + +function truncate(s: string | undefined, max: number): string { + if (!s) return ""; + return s.length <= max ? s : s.slice(0, max - 1) + "…"; +} + +export class AgentLogStore { + private readonly client: MongoClient; + private readonly dbName: string; + private readonly collectionName: string; + private connected = false; + private connectPromise: Promise | null = null; + + constructor(opts: { + url: string; + database: string; + collection?: string; + client?: MongoClient; + }) { + this.client = opts.client ?? new MongoClient(opts.url); + this.dbName = opts.database; + this.collectionName = opts.collection ?? "agent_logs"; + } + + private async coll(): Promise> { + if (!this.connected) { + if (!this.connectPromise) { + this.connectPromise = this.client.connect().then(() => { + this.connected = true; + }); + } + await this.connectPromise; + } + return this.client.db(this.dbName).collection(this.collectionName); + } + + /** + * Append one entry. Best-effort — callers should not let a log failure + * break the user-facing flow. The MongoTelemetry impl wraps this in + * `safeFire` already. + */ + async append(entry: NewAgentLog): Promise { + const doc: AgentLogEntry = { + _id: `log_${randomUUID()}`, + ts: entry.ts ?? new Date(), + source: entry.source, + agentName: entry.agentName, + requester: entry.requester ?? null, + channel: entry.channel ?? null, + threadTs: entry.threadTs ?? null, + sessionId: entry.sessionId ?? null, + query: truncate(entry.query, QUERY_MAX), + reply: truncate(entry.reply, REPLY_MAX), + ok: entry.ok, + error: entry.error, + durationMs: entry.durationMs, + inputTokens: entry.inputTokens, + outputTokens: entry.outputTokens, + costUsd: entry.costUsd ?? null, + }; + await (await this.coll()).insertOne(doc); + } + + /** Most-recent entries first. */ + async list(filter: AgentLogFilter = {}): Promise { + const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500); + const q: Record = {}; + if (filter.agentName) q.agentName = filter.agentName; + if (filter.source) q.source = filter.source; + if (filter.ok !== undefined) q.ok = filter.ok; + if (filter.before) q.ts = { $lt: filter.before }; + return await (await this.coll()).find(q).sort({ ts: -1 }).limit(limit).toArray(); + } + + /** Number of entries matching the filter. */ + async count(filter: Omit = {}): Promise { + const q: Record = {}; + if (filter.agentName) q.agentName = filter.agentName; + if (filter.source) q.source = filter.source; + if (filter.ok !== undefined) q.ok = filter.ok; + return await (await this.coll()).countDocuments(q); + } + + /** Release the underlying MongoClient connection. Idempotent. */ + async close(): Promise { + if (this.connected) { + await this.client.close(); + this.connected = false; + } + } +} diff --git a/packages/agent-registry-mongo/src/index.ts b/packages/agent-registry-mongo/src/index.ts new file mode 100644 index 0000000..ba9d7e7 --- /dev/null +++ b/packages/agent-registry-mongo/src/index.ts @@ -0,0 +1,14 @@ +export { + AgentRegistry, + type AgentRegistryDoc, + type AgentRegistrySpec, +} from "./registry.js"; +export { + AgentLogStore, + QUERY_MAX, + REPLY_MAX, + type AgentLogEntry, + type AgentLogFilter, + type NewAgentLog, +} from "./audit-log.js"; +export { MongoTelemetry, type MongoTelemetryOptions } from "./telemetry.js"; diff --git a/packages/agent-registry-mongo/src/registry.ts b/packages/agent-registry-mongo/src/registry.ts new file mode 100644 index 0000000..df4f2ab --- /dev/null +++ b/packages/agent-registry-mongo/src/registry.ts @@ -0,0 +1,133 @@ +/** + * `agent_registry` collection — one document per registered agent. + * + * Schema (single shared shape across reads + writes): + * + * { + * _id: string, // agent name (unique key) + * label?: string, // pretty name for the dashboard + * harness: string, // "claude-agent-sdk" | "gitagent" | ... + * source: unknown, // IdentitySource (git/local/inline) — JSON-safe shape + * model?: string, // resolved model id (best-effort) + * registeredBy?: string, // free-form: hostname, pod name, "ci", ... + * registeredAt: Date, // first time we saw this name (created) + * updatedAt: Date, // last write + * lastSeen: Date, // most recent ComputerAgent construct + * } + * + * The dashboard reads this collection; the SDK upserts to it via + * `MongoTelemetry.onAgentConstructed`. Tests and tooling can also write + * directly via this class. + */ +import { MongoClient, type Collection, type Db } from "mongodb"; + +export interface AgentRegistrySpec { + /** Stable agent name. Used as the doc _id; uniquely identifies the agent. */ + readonly name: string; + readonly label?: string; + readonly harness: string; + readonly source: unknown; + readonly model?: string; + readonly registeredBy?: string; +} + +export interface AgentRegistryDoc extends AgentRegistrySpec { + readonly _id: string; + readonly registeredAt: Date; + readonly updatedAt: Date; + readonly lastSeen: Date; +} + +export class AgentRegistry { + private readonly client: MongoClient; + private readonly dbName: string; + private readonly collectionName: string; + private connected = false; + private connectPromise: Promise | null = null; + + constructor(opts: { + url: string; + database: string; + collection?: string; + /** Pass an existing MongoClient instead of `url` to share connection pools. */ + client?: MongoClient; + }) { + this.client = opts.client ?? new MongoClient(opts.url); + this.dbName = opts.database; + this.collectionName = opts.collection ?? "agent_registry"; + } + + private async db(): Promise { + if (!this.connected) { + if (!this.connectPromise) { + this.connectPromise = this.client.connect().then(() => { + this.connected = true; + }); + } + await this.connectPromise; + } + return this.client.db(this.dbName); + } + + private async coll(): Promise> { + return (await this.db()).collection(this.collectionName); + } + + /** + * Idempotent upsert. First write sets `registeredAt`; every subsequent + * write only refreshes `updatedAt`, `lastSeen`, and any changed metadata. + * Safe to call on every ComputerAgent construct. + */ + async register(spec: AgentRegistrySpec): Promise { + const now = new Date(); + const set: Partial = { + label: spec.label, + harness: spec.harness, + source: spec.source, + model: spec.model, + registeredBy: spec.registeredBy, + updatedAt: now, + lastSeen: now, + }; + // Strip undefined so MongoDB doesn't store explicit nulls. + for (const k of Object.keys(set) as (keyof typeof set)[]) { + if (set[k] === undefined) delete set[k]; + } + await ( + await this.coll() + ).updateOne( + { _id: spec.name }, + { + $set: set, + $setOnInsert: { _id: spec.name, registeredAt: now }, + }, + { upsert: true }, + ); + } + + /** Remove one agent from the registry. */ + async unregister(name: string): Promise { + await (await this.coll()).deleteOne({ _id: name }); + } + + async get(name: string): Promise { + return await (await this.coll()).findOne({ _id: name }); + } + + /** All registered agents, newest `updatedAt` first. */ + async list(): Promise { + return await ( + await this.coll() + ) + .find({}, { sort: { updatedAt: -1 } }) + .toArray(); + } + + /** Release the underlying MongoClient connection. Idempotent. */ + async close(): Promise { + if (this.connected) { + await this.client.close(); + this.connected = false; + } + } +} diff --git a/packages/agent-registry-mongo/src/telemetry.ts b/packages/agent-registry-mongo/src/telemetry.ts new file mode 100644 index 0000000..3c1b32a --- /dev/null +++ b/packages/agent-registry-mongo/src/telemetry.ts @@ -0,0 +1,207 @@ +/** + * `MongoTelemetry` — drop-in `AgentTelemetry` implementation that writes + * to the MongoDB collections AgentOS reads: + * + * - `agent_registry` → one doc per agent (upserted on `onAgentConstructed`) + * - `agent_logs` → one doc per chat turn (appended on `onChatEnd`) + * + * Designed for the library-mode deployment shape: the customer drops + * `computeragent` into their existing worker (e.g. a Temporal worker pod), + * passes a `MongoTelemetry` to the `ComputerAgent` constructor, and the + * AgentOS dashboard sees every run with no extra orchestration. + * + * Usage (single-line additive): + * + * import { ComputerAgent, LocalSubstrate } from "computeragent"; + * import { MongoTelemetry } from "@computeragent/agent-registry-mongo"; + * + * const telemetry = new MongoTelemetry({ + * url: process.env.MONGO_URL!, + * database: "agentos", + * agent: { + * name: "devsupport-agent", + * source: "github.com/nord/devsupport-agent", + * harness: "claude-agent-sdk", + * model: "bedrock/anthropic.claude-sonnet-4-...", + * }, + * }); + * + * await using agent = new ComputerAgent({ + * ..., + * telemetry, // ← that's it + * }); + * + * Telemetry exceptions never propagate up to the agent run (the SDK wraps + * every call in `safeFireTelemetry`), but we also catch and log them + * locally here for diagnostics. + */ +import type { + AgentTelemetry, + AgentConstructedInfo, + ChatEndInfo, + ChatStartInfo, +} from "@computeragent/sdk"; +import { MongoClient } from "mongodb"; +import { AgentLogStore } from "./audit-log.js"; +import { AgentRegistry, type AgentRegistrySpec } from "./registry.js"; + +export interface MongoTelemetryOptions { + /** Mongo connection URL. */ + readonly url: string; + /** Database name (e.g. "agentos"). */ + readonly database: string; + /** + * Identity of the agent this telemetry instance represents. `name` is + * required (it's the primary key in `agent_registry` and the dashboard + * grouping key in `agent_logs`). Other fields are taken from the SDK + * lifecycle if you omit them — `name` is the only thing the SDK can't + * synthesize on its own. + */ + readonly agent: { + readonly name: string; + readonly label?: string; + readonly harness?: string; // overridden by SDK's onAgentConstructed.harness + readonly model?: string; // overridden by SDK's onAgentConstructed.model + readonly source?: unknown; // overridden by SDK's onAgentConstructed.source + readonly registeredBy?: string; // free-form (hostname, pod name, ...) + }; + /** + * Tag that lands in `agent_logs.source` — useful for distinguishing where + * a run came from in the dashboard. Defaults to "library". + */ + readonly source?: string; + /** Override the agent_registry collection name. */ + readonly registryCollection?: string; + /** Override the agent_logs collection name. */ + readonly logsCollection?: string; + /** + * Optional shared MongoClient — pass one if the customer already manages a + * pool. We won't .close() it on dispose; the caller owns it. + */ + readonly client?: MongoClient; + /** Optional logger for telemetry diagnostics. Defaults to noop. */ + readonly onError?: (err: unknown, op: string) => void; +} + +/** Per-chat context the SDK threads from `onChatStart` to `onChatEnd`. */ +interface ChatCtx { + readonly startedAt: number; + readonly message: string; +} + +export class MongoTelemetry implements AgentTelemetry { + private readonly opts: MongoTelemetryOptions; + private readonly registry: AgentRegistry; + private readonly logs: AgentLogStore; + private readonly source: string; + private readonly ownsClient: boolean; + private readonly logErr: (err: unknown, op: string) => void; + + constructor(opts: MongoTelemetryOptions) { + this.opts = opts; + this.source = opts.source ?? "library"; + this.ownsClient = opts.client === undefined; + this.logErr = + opts.onError ?? + ((err, op) => { + // Default: print one line to stderr; never throw. + try { + // eslint-disable-next-line no-console + console.error( + `[agent-registry-mongo] ${op} failed:`, + err instanceof Error ? err.message : String(err), + ); + } catch { + /* swallow */ + } + }); + + const sharedClient = opts.client; + this.registry = new AgentRegistry({ + url: opts.url, + database: opts.database, + collection: opts.registryCollection, + client: sharedClient, + }); + this.logs = new AgentLogStore({ + url: opts.url, + database: opts.database, + collection: opts.logsCollection, + client: sharedClient, + }); + } + + // ── AgentTelemetry impl ────────────────────────────────────────────────── + + async onAgentConstructed(info: AgentConstructedInfo): Promise { + const spec: AgentRegistrySpec = { + name: this.opts.agent.name, + label: this.opts.agent.label, + // The SDK knows the harness + source + model authoritatively. Override + // whatever the caller passed at construction with the SDK truth, but + // fall back to the caller's value if the SDK hint is undefined. + harness: info.harness ?? this.opts.agent.harness ?? "unknown", + source: info.source ?? this.opts.agent.source ?? null, + model: info.model ?? this.opts.agent.model, + registeredBy: this.opts.agent.registeredBy, + }; + try { + await this.registry.register(spec); + } catch (err) { + this.logErr(err, "onAgentConstructed"); + } + } + + onChatStart(info: ChatStartInfo): ChatCtx { + return { startedAt: Date.now(), message: info.message }; + } + + async onChatEnd(info: ChatEndInfo): Promise { + const ctx = info.context as ChatCtx | undefined; + try { + await this.logs.append({ + source: this.source, + agentName: this.opts.agent.name, + requester: this.source === "library" ? "library" : null, + channel: null, + threadTs: null, + sessionId: info.sessionId || null, + query: ctx?.message ?? "", + reply: info.reply ?? "", + ok: info.ok, + error: info.error, + durationMs: + info.durationMs ?? + (ctx?.startedAt !== undefined ? Date.now() - ctx.startedAt : undefined), + inputTokens: info.usage?.inputTokens, + outputTokens: info.usage?.outputTokens, + costUsd: info.usage?.costUsd ?? null, + }); + } catch (err) { + this.logErr(err, "onChatEnd"); + } + } + + async onClose(): Promise { + // If we don't own the MongoClient (caller passed one in), respect their + // lifecycle and leave it alone. + if (!this.ownsClient) return; + try { + await Promise.all([this.registry.close(), this.logs.close()]); + } catch (err) { + this.logErr(err, "onClose"); + } + } + + // ── Convenience accessors so the customer can read the same data they write ── + + /** Direct access to the registry collection (CRUD beyond the telemetry hook). */ + get registryStore(): AgentRegistry { + return this.registry; + } + + /** Direct access to the audit log collection. */ + get logStore(): AgentLogStore { + return this.logs; + } +} diff --git a/packages/agent-registry-mongo/tsconfig.json b/packages/agent-registry-mongo/tsconfig.json new file mode 100644 index 0000000..2d60705 --- /dev/null +++ b/packages/agent-registry-mongo/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/sdk/src/computer-agent.ts b/packages/sdk/src/computer-agent.ts index 4b0f6e3..9c3f6ea 100644 --- a/packages/sdk/src/computer-agent.ts +++ b/packages/sdk/src/computer-agent.ts @@ -12,11 +12,55 @@ import { ChatHandle } from "./chat-handle.js"; import { asHarnessError } from "./errors.js"; import { consumeSseEvents } from "./sse-client.js"; import type { Substrate, BootedHarness } from "./substrate.js"; -import type { ChatInput, ComputerAgentOptions, PermissionDecision, ToolCallContext } from "./types.js"; +import type { AgentTelemetry } from "./telemetry.js"; +import type { + ChatInput, + ChatResult, + ComputerAgentOptions, + PermissionDecision, + ToolCallContext, +} from "./types.js"; const DEFAULT_HARNESS_URL = "http://127.0.0.1:7700"; const DEFAULT_LOADER = "gitagentprotocol"; +/** Run a telemetry callback fire-and-forget — never let telemetry break a chat. */ +function safeFireTelemetry(fn: (() => void | Promise) | undefined): void { + if (!fn) return; + try { + const r = fn(); + if (r && typeof (r as Promise).then === "function") { + (r as Promise).catch(() => {}); + } + } catch { + /* swallow — telemetry must never break an agent run */ + } +} + +/** Best-effort extract user message text from a ChatInput for the telemetry hook. */ +function extractMessageText(input: ChatInput): string { + if (typeof input === "string") return input; + if (Array.isArray(input)) { + const first = input.find((m) => m && typeof m === "object"); + if (first && typeof (first as { content?: unknown }).content === "string") { + return (first as { content: string }).content; + } + return JSON.stringify(input).slice(0, 500); + } + return JSON.stringify(input).slice(0, 500); +} + +/** Best-effort extract final assistant text from a ChatResult for the telemetry hook. */ +function extractReplyText(result: ChatResult): string | undefined { + for (let i = result.messages.length - 1; i >= 0; i--) { + const m = result.messages[i] as Record | undefined; + if (!m || typeof m !== "object") continue; + if (typeof m.content === "string") return m.content; + if (typeof (m as { text?: unknown }).text === "string") return (m as { text: string }).text; + } + return undefined; +} + function isSubstrate(r: unknown): r is Substrate { return typeof r === "object" && r !== null && typeof (r as Substrate).bootHarness === "function"; } @@ -128,8 +172,27 @@ export class ComputerAgent { } else { this.effectiveOptions = undefined; } + + // ── Fire telemetry: this agent now exists. ──────────────────────────── + // The Mongo telemetry impl upserts a doc into `agent_registry` so the + // AgentOS dashboard shows the agent immediately, even in library-mode + // deployments where the SDK is just an npm dep inside the customer's + // worker. Fire-and-forget; telemetry exceptions never propagate. + this.telemetry = opts.telemetry; + if (this.telemetry?.onAgentConstructed) { + safeFireTelemetry(() => + this.telemetry!.onAgentConstructed!({ + source: this.source, + harness: opts.harness, + model: opts.model, + baseUrl: opts.baseUrl, + }), + ); + } } + private readonly telemetry: AgentTelemetry | undefined; + /** Stable session id. Available only after the first `.chat()` (or if explicitly passed). */ get sessionId(): string | undefined { return this.existingSessionId; @@ -159,6 +222,8 @@ export class ComputerAgent { this.bootingPromise = null; await b.shutdown(); } + // Release telemetry resources (e.g. close a Mongo client). Fire-and-forget. + if (this.telemetry?.onClose) safeFireTelemetry(() => this.telemetry!.onClose!()); } /** @@ -318,7 +383,7 @@ export class ComputerAgent { const events = this.openTurnEventStream(sessionIdPromise, harnessUrlPromise); const onPerm = this.opts.onToolCall ? this.wrapOnToolCall(this.opts.onToolCall) : undefined; - return new ChatHandle({ + const handle = new ChatHandle({ sessionIdPromise, events, harnessUrlPromise, @@ -326,6 +391,59 @@ export class ComputerAgent { onPermissionRequest: onPerm, logger: this.logger, }); + + // ── Telemetry: fire onChatStart now, attach onChatEnd to the result ── + // ChatHandle.then() is memoized via result() — so attaching our own + // .then alongside the caller's await is safe; both consumers see the + // same resolved value. Fire-and-forget; never break the chat. + if (this.telemetry) { + const t0 = Date.now(); + let ctx: unknown = undefined; + if (this.telemetry.onChatStart) { + try { + ctx = this.telemetry.onChatStart({ + sessionIdPromise, + message: extractMessageText(input), + }); + } catch { + /* swallow */ + } + } + if (this.telemetry.onChatEnd) { + const tel = this.telemetry; + Promise.resolve(handle as PromiseLike).then( + (result) => { + safeFireTelemetry(() => + tel.onChatEnd!({ + context: ctx, + sessionId: result.sessionId, + ok: true, + durationMs: Date.now() - t0, + usage: { + inputTokens: result.usage.inputTokens, + outputTokens: result.usage.outputTokens, + costUsd: result.usage.costUsd, + }, + reply: extractReplyText(result), + }), + ); + }, + (err: unknown) => { + safeFireTelemetry(() => + tel.onChatEnd!({ + context: ctx, + sessionId: this.existingSessionId ?? "", + ok: false, + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - t0, + }), + ); + }, + ); + } + } + + return handle; } // ── private ───────────────────────────────────────────────────────────── diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 9a23f32..96fde20 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -15,6 +15,12 @@ export type { UsageRollup, } from "./types.js"; export type { Substrate, BootHarnessOptions, BootedHarness } from "./substrate.js"; +export type { + AgentTelemetry, + AgentConstructedInfo, + ChatStartInfo, + ChatEndInfo, +} from "./telemetry.js"; export type { FsTreeEntry, HarnessEvent, IdentitySource, UserMessage } from "@computeragent/protocol"; export { HarnessProtocolError, diff --git a/packages/sdk/src/telemetry.ts b/packages/sdk/src/telemetry.ts new file mode 100644 index 0000000..ab29e8f --- /dev/null +++ b/packages/sdk/src/telemetry.ts @@ -0,0 +1,72 @@ +/** + * `AgentTelemetry` — the hook the SDK calls so an external collector can + * register the agent in a dashboard registry and record every chat turn. + * + * Designed for one concrete deployment shape: ComputerAgent runs as a + * *library* inside the customer's existing worker (e.g. a Temporal worker + * pod), not as a separate HTTP service. In that shape there's no central + * server collecting traces — the SDK itself has to emit them. Pass an + * `AgentTelemetry` and we'll fire these hooks from the lifecycle. + * + * The first-class implementation is `@computeragent/agent-registry-mongo` + * which writes to the `agent_registry` and `agent_logs` Mongo collections + * that AgentOS reads. Anything else (Honeycomb, OTel, Lyzr Trace, custom + * HTTP push) is a small adapter on top of this interface. + * + * Contract: + * - All methods are optional. Implementations can subscribe to a subset. + * - All methods return `void | Promise`. The SDK fires them + * fire-and-forget — exceptions are caught and logged at debug level + * but never propagate up. Telemetry must never break an agent run. + * - Hooks fire in order: `onAgentConstructed` once at construction, + * `onChatStart` + `onChatEnd` paired per chat() call, `onClose` once + * on `dispose()`. + * - `onChatStart` may return an opaque context that is passed back to + * `onChatEnd`. Useful for stashing per-chat timer state. + */ +import type { IdentitySource } from "@computeragent/protocol"; + +export interface AgentConstructedInfo { + readonly source: IdentitySource; + readonly harness: string; + readonly model?: string; + /** Best-effort effective base URL the agent will reach for the LLM. */ + readonly baseUrl?: string; +} + +export interface ChatStartInfo { + /** Resolves once the session is registered on the harness. */ + readonly sessionIdPromise: Promise; + /** User message text (or first message if a multi-part input). */ + readonly message: string; +} + +export interface ChatEndInfo { + /** Whatever `onChatStart` returned. The implementation defines the shape. */ + readonly context: unknown; + readonly sessionId: string; + readonly ok: boolean; + readonly error?: string; + readonly durationMs: number; + readonly usage?: { + readonly inputTokens: number; + readonly outputTokens: number; + readonly costUsd?: number; + }; + /** Final assistant text, if extractable. Implementations may truncate. */ + readonly reply?: string; +} + +export interface AgentTelemetry { + /** Fires once when `new ComputerAgent(...)` is constructed. */ + onAgentConstructed?(info: AgentConstructedInfo): void | Promise; + + /** Fires at the start of each `agent.chat(msg)` call. */ + onChatStart?(info: ChatStartInfo): unknown; + + /** Fires when a chat completes (success or failure). */ + onChatEnd?(info: ChatEndInfo): void | Promise; + + /** Fires from `agent.dispose()`. Should release any open resources (DB clients). */ + onClose?(): void | Promise; +} diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 789bede..c721adf 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -7,6 +7,7 @@ import type { UserMessage, } from "@computeragent/protocol"; import type { Substrate } from "./substrate.js"; +import type { AgentTelemetry } from "./telemetry.js"; /** Convenience: any input shape accepted by `agent.chat()`. */ export type ChatInput = @@ -142,6 +143,19 @@ export interface ComputerAgentOptions { * ] */ readonly attachments?: readonly Attachment[]; + /** + * Optional telemetry hook. When supplied, the SDK fires `onAgentConstructed` + * once at construction, `onChatStart`/`onChatEnd` paired around each chat, + * and `onClose` from `dispose()`. The first-class implementation is + * `@computeragent/agent-registry-mongo` which writes to the `agent_registry` + * and `agent_logs` collections AgentOS reads. Telemetry exceptions are + * caught and never propagate — telemetry must never break an agent run. + * + * This is the hook that makes library-mode deployments observable: when a + * customer just `npm install computeragent` into their existing worker and + * passes a Mongo-backed telemetry, the AgentOS dashboard sees every run. + */ + readonly telemetry?: AgentTelemetry; /** * When true, sets `COMPUTERAGENT_LOG=debug` in the harness env (forcing * every engine + framework log line to surface) and emits one client-side From 5589a23bd552f73d30bd9509b0677b4c03411653 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 05:26:33 -0400 Subject: [PATCH 2/9] =?UTF-8?q?feat(agentos,scripts):=20Mongo-backed=20reg?= =?UTF-8?q?istry=20=E2=80=94=20dashboard=20+=20CRUD=20+=20SPA=20+=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the dashboard side of "library-mode tracking": the agents collection in Mongo (written by the SDK's MongoTelemetry hook) is read by the dashboard and unioned with the server's hardcoded in-memory list. examples/agentos-api.ts: - New `agent_registry` collection accessor. - GET /agentos/api/agents now unions in-memory + registry rows; in-memory wins on name collision so server-hosted agents (Slack bots, framework- translator, etc) keep authoritative wiring. Each row carries `origin` ("in-memory" | "registry"), `registeredBy`, and `lastSeen` so the dashboard can label library-mode entries. - POST /agentos/api/agents/register — upsert by name (idempotent). - PATCH /agentos/api/agents/:name — update label/harness/source/model (rejects names that exist in the in-memory list). - DELETE /agentos/api/agents/:name — remove from registry only. agentos/src/components/RegisterAgentForm.tsx: - Inline "+ Register agent" widget in the agent rail. Name + source are required; harness defaults to claude-agent-sdk. POSTs /agents/register and refreshes the agent list on success. agentos/src/App.tsx + api.ts: - New `origin`/`registeredBy`/`lastSeen` fields on the Agent type and registerAgent/unregisterAgent/patchAgent methods on the api client. - Renders a small "lib" badge next to registry-origin agents. - Mounts below the agent list. scripts/seed-agent-registry.ts: - One-shot, idempotent. Reads the hardcoded SEED_AGENTS list and upserts each into agent_registry via @computeragent/agent-registry-mongo. - Run once when migrating an existing deployment; safe to re-run. Verified: pnpm sdk + agent-registry-mongo + recursive typecheck all clean, existing SDK tests still pass, agentos SPA Vite build clean (35 modules, 180 kB / 56 kB gzip). --- agentos/src/App.tsx | 14 +- agentos/src/api.ts | 23 +++ agentos/src/components/RegisterAgentForm.tsx | 181 +++++++++++++++++++ examples/agentos-api.ts | 167 ++++++++++++++++- pnpm-lock.yaml | 19 ++ scripts/seed-agent-registry.ts | 100 ++++++++++ 6 files changed, 500 insertions(+), 4 deletions(-) create mode 100644 agentos/src/components/RegisterAgentForm.tsx create mode 100644 scripts/seed-agent-registry.ts diff --git a/agentos/src/App.tsx b/agentos/src/App.tsx index 09aaaf1..b679f72 100644 --- a/agentos/src/App.tsx +++ b/agentos/src/App.tsx @@ -4,6 +4,7 @@ import { LogsTab } from "./components/LogsTab.tsx"; import { WorkspaceTab } from "./components/WorkspaceTab.tsx"; import { SchedulesTab } from "./components/SchedulesTab.tsx"; import { HomePage } from "./components/HomePage.tsx"; +import { RegisterAgentForm } from "./components/RegisterAgentForm.tsx"; type Tab = "chat" | "schedules" | "logs"; type View = "home" | "dashboard"; @@ -128,16 +129,27 @@ export default function App() {
0 ? "bg-emerald-400" : "bg-gray-600"}`} /> {agentNameFromSource(a.source)} + {a.origin === "registry" && ( + + lib + + )}
{a.source}
{a.sessionCount} sessions {a.logCount} logs - {timeAgo(a.lastActivity)} + {timeAgo(a.lastActivity ?? a.lastSeen ?? null)}
))} +
+ api.agents().then(setAgents).catch(() => {})} /> +
)}
agentos.clawagent.sh
diff --git a/agentos/src/api.ts b/agentos/src/api.ts index b3ba235..091ff05 100644 --- a/agentos/src/api.ts +++ b/agentos/src/api.ts @@ -13,6 +13,23 @@ export interface Agent { activeSandboxes: number; lastActivity: string | null; logCount: number; + /** "in-memory" = configured at server startup (Slack bots, built-ins). + * "registry" = registered dynamically via the SDK's MongoTelemetry + * hook or via POST /agents/register. */ + origin?: "in-memory" | "registry"; + /** Free-form attribution (hostname / pod / "seed-script") for registry agents. */ + registeredBy?: string | null; + /** Most recent ComputerAgent construct seen by the SDK telemetry hook. */ + lastSeen?: string | null; +} + +export interface RegisterAgentInput { + name: string; + label?: string; + harness?: string; + source?: string; + model?: string; + registeredBy?: string; } export interface LogEntry { @@ -98,6 +115,12 @@ async function reqJSON(method: string, path: string, body?: unknown): Promise export const api = { agents: () => getJSON<{ agents: Agent[] }>("/agents").then((d) => d.agents), + registerAgent: (input: RegisterAgentInput) => + postJSON<{ ok: boolean; name: string }>("/agents/register", input), + unregisterAgent: (name: string) => + reqJSON<{ ok: boolean }>("DELETE", `/agents/${encodeURIComponent(name)}`), + patchAgent: (name: string, fields: Partial>) => + reqJSON<{ ok: boolean }>("PATCH", `/agents/${encodeURIComponent(name)}`, fields), logs: (bot?: string, limit = 100) => getJSON<{ logs: LogEntry[] }>(`/logs?limit=${limit}${bot ? `&bot=${encodeURIComponent(bot)}` : ""}`).then((d) => d.logs), sessions: (bot?: string, limit = 100) => diff --git a/agentos/src/components/RegisterAgentForm.tsx b/agentos/src/components/RegisterAgentForm.tsx new file mode 100644 index 0000000..cf7cdf4 --- /dev/null +++ b/agentos/src/components/RegisterAgentForm.tsx @@ -0,0 +1,181 @@ +/** + * Minimal "Register an agent" form. Surfaces the dashboard CRUD against the + * Mongo `agent_registry` collection. Used for ops-driven registration (the + * primary write path is the SDK's MongoTelemetry hook firing automatically + * when a customer's worker imports `computeragent`). + * + * Three fields are required: name, harness, source. Everything else is + * optional. The form upserts via POST /agentos/api/agents/register and + * reports back the agent name on success. + */ +import { useState } from "react"; +import { api, type RegisterAgentInput } from "../api.ts"; + +const HARNESS_OPTIONS = ["claude-agent-sdk", "gitagent", "deepagents"] as const; + +export function RegisterAgentForm({ onRegistered }: { onRegistered?: (name: string) => void }) { + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [label, setLabel] = useState(""); + const [harness, setHarness] = useState(HARNESS_OPTIONS[0]); + const [source, setSource] = useState(""); + const [model, setModel] = useState(""); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(null); + const [ok, setOk] = useState(null); + + const valid = name.trim().length > 0 && source.trim().length > 0; + + const submit = async () => { + setBusy(true); + setErr(null); + setOk(null); + try { + const body: RegisterAgentInput = { + name: name.trim(), + harness, + source: source.trim(), + }; + if (label.trim()) body.label = label.trim(); + if (model.trim()) body.model = model.trim(); + const res = await api.registerAgent(body); + setOk(`Registered "${res.name}"`); + setName(""); + setLabel(""); + setSource(""); + setModel(""); + onRegistered?.(res.name); + } catch (e) { + setErr(String(e)); + } finally { + setBusy(false); + } + }; + + if (!open) { + return ( + + ); + } + + return ( +
+
+
Register an agent
+ +
+

+ Adds an agent to the Mongo agent_registry collection so the dashboard + lists it. For library-mode deployments the SDK's MongoTelemetry hook + registers automatically — use this only for ops-driven registration. +

+ + + + +
+ + +
+ + + + + {err &&
{err}
} + {ok &&
{ok}
} + +
+ + +
+
+ ); +} + +function Field({ + label, + required, + value, + onChange, + placeholder, + autoFocus, +}: { + label: string; + required?: boolean; + value: string; + onChange: (v: string) => void; + placeholder?: string; + autoFocus?: boolean; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full bg-ink-800 border border-ink-700 rounded px-2 py-1.5 text-gray-200 placeholder:text-gray-600" + /> +
+ ); +} diff --git a/examples/agentos-api.ts b/examples/agentos-api.ts index 7d4cd36..6988593 100644 --- a/examples/agentos-api.ts +++ b/examples/agentos-api.ts @@ -86,6 +86,32 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { (await db()).collection("slack_threads"); const sessionsColl = async (): Promise> => (await db()).collection("sessions"); + /** + * agent_registry — agents registered dynamically by SDK consumers via the + * `@computeragent/agent-registry-mongo` telemetry hook (or directly via the + * POST /agentos/api/agents/register endpoint below). The dashboard unions + * these with the server's hardcoded `opts.agents` (in-memory) — the + * in-memory list takes precedence on name collision so the + * harness/source/token wiring stays authoritative for agents this server + * itself runs (Slack bots, framework-translator, etc). + * + * This is what makes library-mode deployments visible: a customer's + * Temporal worker pod runs `new ComputerAgent({telemetry: new MongoTelemetry(...)})` + * and the dashboard shows the agent immediately. + */ + interface RegistryDoc { + _id: string; + label?: string; + harness?: string; + source?: unknown; + model?: string; + registeredBy?: string; + registeredAt?: Date; + updatedAt?: Date; + lastSeen?: Date; + } + const registryColl = async (): Promise> => + (await db()).collection("agent_registry"); const byName = new Map(opts.agents.map((a) => [a.name, a])); const app = new Hono(); @@ -103,8 +129,60 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { } catch { /* best-effort */ } const threads = await threadsColl(); - const out = []; + + // Pull dynamically-registered agents from agent_registry. Best-effort: + // if the collection doesn't exist yet (fresh deployment), or Mongo is + // briefly unavailable, fall back to just the in-memory list. + let registryRows: RegistryDoc[] = []; + try { + registryRows = await (await registryColl()) + .find({}) + .sort({ lastSeen: -1, updatedAt: -1 }) + .toArray(); + } catch { + /* fall through with empty list */ + } + + // Union by name. In-memory wins on collision so server-hosted agents + // (Slack bots, framework-translator, etc) retain their authoritative + // harness/source/token wiring even if a worker registered the same name. + const seen = new Set(opts.agents.map((a) => a.name)); + const combined: Array<{ + name: string; + label: string; + harness: string; + source: unknown; + model: string | null; + origin: "in-memory" | "registry"; + registeredBy?: string; + lastSeen?: Date; + }> = []; for (const a of opts.agents) { + combined.push({ + name: a.name, + label: a.label, + harness: a.harness, + source: a.source, + model: a.model ?? null, + origin: "in-memory", + }); + } + for (const r of registryRows) { + if (seen.has(r._id)) continue; + combined.push({ + name: r._id, + label: r.label ?? r._id, + harness: r.harness ?? "unknown", + source: r.source ?? "", + model: r.model ?? null, + origin: "registry", + registeredBy: r.registeredBy, + lastSeen: r.lastSeen, + }); + } + + const out = []; + for (const a of combined) { const docs = await threads.find({ bot: a.name }).toArray(); const sessionIds = new Set(docs.map((d) => d.sessionId)); let active = 0; @@ -121,8 +199,11 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { label: a.label, harness: a.harness, source: a.source, - model: a.model ?? null, - sandboxCapable: sandboxCapable(a.harness), + model: a.model, + origin: a.origin, + registeredBy: a.registeredBy ?? null, + lastSeen: a.lastSeen ? a.lastSeen.toISOString() : null, + sandboxCapable: a.origin === "in-memory" && sandboxCapable(a.harness), sessionCount: sessionIds.size, activeSandboxes: active, lastActivity: lastActivity ? lastActivity.toISOString() : null, @@ -132,6 +213,86 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { return c.json({ agents: out }); }); + // ── Agent registry CRUD ───────────────────────────────────────────────── + // Registry-side mutations only — the in-memory list configured at server + // startup is never modified by these endpoints. Library-mode SDK consumers + // can also write directly via `MongoTelemetry`; this endpoint is for ops + // tools / manual registration from the dashboard. + + app.post("/agentos/api/agents/register", async (c) => { + const body = (await c.req.json().catch(() => ({}))) as Record; + const name = typeof body.name === "string" ? body.name.trim() : ""; + if (!name) return c.json({ error: { code: "BAD_REQUEST", message: "`name` required" } }, 400); + const now = new Date(); + const set: Partial = { + label: typeof body.label === "string" ? body.label : undefined, + harness: typeof body.harness === "string" ? body.harness : undefined, + source: body.source ?? undefined, + model: typeof body.model === "string" ? body.model : undefined, + registeredBy: typeof body.registeredBy === "string" ? body.registeredBy : undefined, + updatedAt: now, + lastSeen: now, + }; + for (const k of Object.keys(set) as (keyof typeof set)[]) { + if (set[k] === undefined) delete set[k]; + } + await ( + await registryColl() + ).updateOne( + { _id: name }, + { $set: set, $setOnInsert: { _id: name, registeredAt: now } }, + { upsert: true }, + ); + return c.json({ ok: true, name }); + }); + + app.patch("/agentos/api/agents/:name", async (c) => { + const name = c.req.param("name"); + const body = (await c.req.json().catch(() => ({}))) as Record; + if (byName.has(name)) { + return c.json( + { + error: { + code: "IN_MEMORY_AGENT", + message: "This agent is configured at server startup; edit examples/computeragent-server.ts instead.", + }, + }, + 409, + ); + } + const set: Partial = { + label: typeof body.label === "string" ? body.label : undefined, + harness: typeof body.harness === "string" ? body.harness : undefined, + source: body.source, + model: typeof body.model === "string" ? body.model : undefined, + updatedAt: new Date(), + }; + for (const k of Object.keys(set) as (keyof typeof set)[]) { + if (set[k] === undefined) delete set[k]; + } + const r = await (await registryColl()).updateOne({ _id: name }, { $set: set }); + if (r.matchedCount === 0) return c.json({ error: { code: "NOT_FOUND" } }, 404); + return c.json({ ok: true }); + }); + + app.delete("/agentos/api/agents/:name", async (c) => { + const name = c.req.param("name"); + if (byName.has(name)) { + return c.json( + { + error: { + code: "IN_MEMORY_AGENT", + message: "This agent is configured at server startup; remove it from examples/computeragent-server.ts and restart.", + }, + }, + 409, + ); + } + const r = await (await registryColl()).deleteOne({ _id: name }); + if (r.deletedCount === 0) return c.json({ error: { code: "NOT_FOUND" } }, 404); + return c.json({ ok: true }); + }); + // ── Request logs ──────────────────────────────────────────────────────── app.get("/agentos/api/logs", async (c) => { const bot = c.req.query("bot") || undefined; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c26d97a..d9a39e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,25 @@ importers: specifier: ^3.1.3 version: 3.1.4 + packages/agent-registry-mongo: + dependencies: + '@computeragent/protocol': + specifier: workspace:* + version: link:../protocol + '@computeragent/sdk': + specifier: workspace:* + version: link:../sdk + mongodb: + specifier: ^6.10.0 + version: 6.21.0(socks@2.8.9) + devDependencies: + typescript: + specifier: ^5.5.0 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@22.19.18) + packages/cli: dependencies: '@computeragent/sdk': diff --git a/scripts/seed-agent-registry.ts b/scripts/seed-agent-registry.ts new file mode 100644 index 0000000..841197e --- /dev/null +++ b/scripts/seed-agent-registry.ts @@ -0,0 +1,100 @@ +/** + * Idempotent one-shot — seed the `agent_registry` Mongo collection with the + * agents currently configured in-memory at `examples/computeragent-server.ts`. + * + * Run once when migrating an existing deployment to the registry-backed + * dashboard. The dashboard reads `agent_registry` and unions with the + * in-memory list, so this seed isn't strictly required (the dashboard still + * works without it) — but it lets you tweak label/model/source via the + * dashboard CRUD endpoints without restarting the server. + * + * MONGO_URL=mongodb://... MONGO_DATABASE=agentos \ + * pnpm tsx scripts/seed-agent-registry.ts + * + * Safe to re-run: `register()` is an idempotent upsert by name. + */ +import { MongoClient } from "mongodb"; +import { AgentRegistry } from "@computeragent/agent-registry-mongo"; + +interface SeedAgent { + readonly name: string; + readonly label: string; + readonly harness: string; + readonly source: string; + readonly model?: string; +} + +// Mirrors the hardcoded agentDefs in examples/computeragent-server.ts:2306+. +// Keep in sync when adding/removing built-in agents there. +const SEED_AGENTS: readonly SeedAgent[] = [ + { + name: "gitagent", + label: "GitAgent", + harness: "gitagent", + source: "github.com/shreyas-lyzr/general-agent", + }, + { + name: "claude-code", + label: "Claude Code", + harness: "claude-agent-sdk", + source: "github.com/shreyas-lyzr/general-agent", + }, + { + name: "deep-agent", + label: "Deep Agent", + harness: "deepagents", + source: "github.com/shreyas-lyzr/general-agent", + }, + { + name: "agentosbuilder", + label: "AgentOS Builder", + harness: "claude-agent-sdk", + source: "github.com/open-gitagent/agentos-builder", + }, + { + name: "gap-promoter", + label: "GAP Promoter", + harness: "gitagent", + source: "github.com/open-gitagent/gap-promoter", + }, + { + name: "framework-translator", + label: "Framework Translator", + harness: "gitagent", + source: "github.com/shreyas-lyzr/framework-translator-agent", + }, +]; + +async function main(): Promise { + const url = process.env.MONGO_URL; + const database = process.env.MONGO_DATABASE ?? "agentos"; + if (!url) { + console.error("MONGO_URL is required"); + process.exit(2); + } + + const client = new MongoClient(url); + await client.connect(); + const registry = new AgentRegistry({ url, database, client }); + + console.log(`[seed] writing ${SEED_AGENTS.length} agents to ${database}.agent_registry`); + for (const a of SEED_AGENTS) { + await registry.register({ + name: a.name, + label: a.label, + harness: a.harness, + source: a.source, + model: a.model, + registeredBy: "seed-script", + }); + console.log(` ✓ ${a.name}`); + } + + await client.close(); + console.log("[seed] done"); +} + +main().catch((err) => { + console.error("[seed] fatal:", err); + process.exit(1); +}); From d782cf1109abc9393feede94c672ead71dfafdd3 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 05:46:25 -0400 Subject: [PATCH 3/9] feat(engine,agentos): Bedrock env passthrough (2a) + git URL as agent identity (2b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2a — packages/engine-claude-agent-sdk/src/engine.ts: inheritEssentialHostEnv() now also propagates the AWS Bedrock + IRSA chain to the spawned harness subprocess: CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_DEFAULT_REGION, AWS_BEDROCK_MODEL_ID, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_PROFILE, AWS_SHARED_CREDENTIALS_FILE, AWS_CONFIG_FILE. Function is now exported for testability. 3 new tests (engine.test.ts) pin the allowlist + verify no empty-string leaks; suite goes from 7 → 10 tests, all green. Phase 2b — git URL as canonical agent identity in the dashboard: examples/agentos-api.ts - Import IdentitySource from @computeragent/protocol; new normalizeSource() narrows the registry's `source: unknown` into either {source: IdentitySource, sourceUrl: string} (clickable repo URL) or {source: string, sourceUrl: string} (legacy in-memory agents). - GET /agentos/api/agents now returns both fields per agent. - NEW GET /agentos/api/agents/by-source?url= — looks up agents (in-memory + registry) sharing a source URL; used to detect "same git repo, two workers registered separately". agentos/src/api.ts - Widen Agent.source to IdentitySource | string + add sourceUrl: string | null. - New displaySource() helper returns {kind, primary, secondary, href?} — parses github/gitlab/bitbucket URLs into owner/repo, falls back gracefully. agentos/src/components/SourceBadge.tsx (NEW) - Renders a kind-glyph (github octocat for git, folder for local, ⟪⟫ for inline) + owner/repo headline + host subtitle. For git sources, wraps in opening the repo in a new tab. e.stopPropagation so clicking the link doesn't trigger the parent agent-row click. agentos/src/App.tsx - Replace the 11px gray source line with . - agentNameFromSource() now receives sourceUrl (always a string) instead of the widened source field. Verified: pnpm -r typecheck clean; engine-claude-agent-sdk 10 tests pass; agentos Vite build clean (36 modules, 183 kB / 58 kB gzip; +1 module +3 kB for SourceBadge). --- agentos/src/App.tsx | 9 +- agentos/src/api.ts | 73 +++++++++++++++- agentos/src/components/SourceBadge.tsx | 86 +++++++++++++++++++ examples/agentos-api.ts | 59 ++++++++++++- .../src/engine.test.ts | 81 +++++++++++++++++ .../engine-claude-agent-sdk/src/engine.ts | 23 ++++- 6 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 agentos/src/components/SourceBadge.tsx diff --git a/agentos/src/App.tsx b/agentos/src/App.tsx index b679f72..735eda0 100644 --- a/agentos/src/App.tsx +++ b/agentos/src/App.tsx @@ -5,6 +5,7 @@ import { WorkspaceTab } from "./components/WorkspaceTab.tsx"; import { SchedulesTab } from "./components/SchedulesTab.tsx"; import { HomePage } from "./components/HomePage.tsx"; import { RegisterAgentForm } from "./components/RegisterAgentForm.tsx"; +import { SourceBadge } from "./components/SourceBadge.tsx"; type Tab = "chat" | "schedules" | "logs"; type View = "home" | "dashboard"; @@ -128,7 +129,7 @@ export default function App() { >
0 ? "bg-emerald-400" : "bg-gray-600"}`} /> - {agentNameFromSource(a.source)} + {agentNameFromSource(a.sourceUrl ?? "")} {a.origin === "registry" && (
-
{a.source}
+
+ +
{a.sessionCount} sessions {a.logCount} logs @@ -164,7 +167,7 @@ export default function App() {
- {agentNameFromSource(agent.source)} + {agentNameFromSource(agent.sourceUrl ?? "")}
diff --git a/agentos/src/api.ts b/agentos/src/api.ts index 091ff05..24ade74 100644 --- a/agentos/src/api.ts +++ b/agentos/src/api.ts @@ -2,11 +2,25 @@ // and handles auth (the subdomain is gated by Caddy basic_auth). In dev, Vite // proxies /api with an injected Basic Auth header. So the bundle never holds creds. +/** Mirrors the protocol's IdentitySource zod schema. The server narrows on + * read; the dashboard renders the structured form when present. */ +export type IdentitySource = + | { type: "git"; url: string; ref?: string; subdir?: string } + | { type: "local"; path: string } + | { type: "inline"; manifest: Record; files?: Record }; + export interface Agent { name: string; label: string; harness: string; - source: string; + /** Structured IdentitySource for registry agents (preferred); legacy string + * for in-memory agents from the hardcoded config. Use `sourceUrl` for the + * canonical identity / display URL. */ + source: IdentitySource | string; + /** Canonical URL/path for this agent. Git: the repo URL. Local: the path. + * Inline: the literal string "inline". The dashboard treats this as the + * de-duplication key alongside `name`. */ + sourceUrl: string | null; model: string | null; sandboxCapable: boolean; sessionCount: number; @@ -23,6 +37,63 @@ export interface Agent { lastSeen?: string | null; } +/** Result of `displaySource(agent.source)`. Drives `` rendering. */ +export interface SourceDisplay { + kind: "git" | "local" | "inline" | "unknown"; + /** Headline (e.g. "open-gitagent/ComputerAgent" for git). */ + primary: string; + /** Subtitle (e.g. "github.com" host or full path). */ + secondary: string; + /** `https://` URL to open when clicked (git only). */ + href?: string; +} + +/** + * Derive a render-ready breakdown of an agent's source. Recognizes common + * git hosts (github.com / gitlab.com / bitbucket.org / open-gitagent.dev, + * with or without a leading scheme) and splits owner/repo. Falls back + * gracefully for unrecognized forms. + */ +export function displaySource(source: IdentitySource | string | null | undefined): SourceDisplay { + if (!source) return { kind: "unknown", primary: "(no source)", secondary: "" }; + + if (typeof source === "object") { + if (source.type === "local") { + const path = source.path; + const tail = path.split("/").filter(Boolean).slice(-2).join("/"); + return { kind: "local", primary: tail || path, secondary: path }; + } + if (source.type === "inline") { + const name = + (typeof source.manifest?.name === "string" && (source.manifest.name as string)) || + "inline"; + return { kind: "inline", primary: name, secondary: "inline manifest" }; + } + // git + return parseGitUrl(source.url, source.ref); + } + // Legacy: bare string. Treat as a git-style "host/owner/repo". + return parseGitUrl(source); +} + +function parseGitUrl(raw: string, ref?: string): SourceDisplay { + // Strip protocol + trailing .git + const stripped = raw.replace(/^https?:\/\//, "").replace(/^git@/, "").replace(/\.git$/, ""); + const parts = stripped.split(/[:/]/).filter(Boolean); + // host/owner/repo + if (parts.length >= 3) { + const [host, owner, repo] = parts; + const href = `https://${host}/${owner}/${repo}${ref ? `/tree/${encodeURIComponent(ref)}` : ""}`; + return { kind: "git", primary: `${owner}/${repo}`, secondary: host, href }; + } + // Just "owner/repo" — assume github. + if (parts.length === 2) { + const [owner, repo] = parts; + return { kind: "git", primary: `${owner}/${repo}`, secondary: "github.com", href: `https://github.com/${owner}/${repo}` }; + } + return { kind: "unknown", primary: raw, secondary: "" }; +} + export interface RegisterAgentInput { name: string; label?: string; diff --git a/agentos/src/components/SourceBadge.tsx b/agentos/src/components/SourceBadge.tsx new file mode 100644 index 0000000..a5121e8 --- /dev/null +++ b/agentos/src/components/SourceBadge.tsx @@ -0,0 +1,86 @@ +/** + * Renders an agent's source as a clickable owner/repo block for git sources, + * and a plain two-line breakdown for local/inline. The git URL is treated as + * the agent's canonical identity — two workers registering the same repo + * under different names share this badge (same `sourceUrl`). + */ +import { type Agent, displaySource } from "../api.ts"; + +export function SourceBadge({ agent, compact = false }: { agent: Agent; compact?: boolean }) { + const d = displaySource(agent.source); + + const inner = ( +
+ +
+
+ {d.primary} +
+ {!compact && ( +
+ {d.secondary} +
+ )} +
+
+ ); + + if (d.href) { + return ( +
e.stopPropagation()} // don't fire the parent button click + className="block rounded px-1 -mx-1 hover:bg-ink-700/60 transition-colors" + title={`Open ${d.primary} on ${d.secondary}`} + > + {inner} + + ); + } + return inner; +} + +function KindGlyph({ kind }: { kind: "git" | "local" | "inline" | "unknown" }) { + if (kind === "git") { + return ( + + ); + } + if (kind === "local") { + return ( + + ); + } + if (kind === "inline") { + return ( + + ); + } + return ; +} diff --git a/examples/agentos-api.ts b/examples/agentos-api.ts index 6988593..e72496b 100644 --- a/examples/agentos-api.ts +++ b/examples/agentos-api.ts @@ -14,11 +14,38 @@ import { Hono } from "hono"; import { MongoClient, type Collection } from "mongodb"; import { randomUUID } from "node:crypto"; +import { IdentitySource, type IdentitySource as IdentitySourceT } from "@computeragent/protocol"; import { sandboxBodyForBot } from "./slack-bot.ts"; import { AgentLogStore } from "./agent-log-store.ts"; import { ScheduleStore, computeNextRun, describeSchedule, type ScheduleKind } from "./schedule-store.ts"; import { runAgentOnce } from "./scheduler.ts"; +/** + * Normalize a stored `source` field (string | IdentitySource | undefined) into a + * `{source, sourceUrl}` pair for the dashboard: + * + * - structured IdentitySource → return as-is + extract the canonical URL/path. + * - bare string → keep as the legacy string form (in-memory agents still + * pass plain `source: "github.com/..."`); URL is derived heuristically. + * + * The dashboard treats `sourceUrl` as the agent's canonical identity (the + * thing that's clickable + the de-duplication key across multiple workers + * that registered the same git source). + */ +function normalizeSource(raw: unknown): { source: IdentitySourceT | string; sourceUrl: string | null } { + if (raw && typeof raw === "object") { + const parsed = IdentitySource.safeParse(raw); + if (parsed.success) { + const s = parsed.data; + if (s.type === "git") return { source: s, sourceUrl: s.url }; + if (s.type === "local") return { source: s, sourceUrl: s.path }; + return { source: s, sourceUrl: "inline" }; + } + } + const str = typeof raw === "string" ? raw : ""; + return { source: str, sourceUrl: str || null }; +} + /** An agent the control panel can list, chat with, and inspect. Covers both * Slack bots and web-only agents. */ export interface AgentDef { @@ -194,11 +221,16 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { const t = d.lastMessageAt ? new Date(d.lastMessageAt) : null; return t && (!acc || t > acc) ? t : acc; }, null); + // Normalize source — structured IdentitySource (from agent_registry + // upserts via MongoTelemetry) becomes a {source, sourceUrl} pair the + // dashboard renders as a clickable owner/repo identity for git sources. + const { source, sourceUrl } = normalizeSource(a.source); out.push({ name: a.name, label: a.label, harness: a.harness, - source: a.source, + source, + sourceUrl, model: a.model, origin: a.origin, registeredBy: a.registeredBy ?? null, @@ -213,6 +245,31 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { return c.json({ agents: out }); }); + // Lookup by source URL — used to detect "same agent, different name" + // (e.g. dev + prod workers both registering the same git repo as separate + // names). The dashboard groups these visually. + app.get("/agentos/api/agents/by-source", async (c) => { + const url = c.req.query("url"); + if (!url) return c.json({ error: { code: "BAD_REQUEST", message: "`url` required" } }, 400); + const matches: Array<{ name: string; source: IdentitySourceT | string; sourceUrl: string | null }> = []; + for (const a of opts.agents) { + const norm = normalizeSource(a.source); + if (norm.sourceUrl === url) matches.push({ name: a.name, source: norm.source, sourceUrl: norm.sourceUrl }); + } + try { + const rows = await (await registryColl()).find({}).toArray(); + for (const r of rows) { + const norm = normalizeSource(r.source); + if (norm.sourceUrl === url) { + matches.push({ name: r._id, source: norm.source, sourceUrl: norm.sourceUrl }); + } + } + } catch { + /* registry collection optional */ + } + return c.json({ url, matches }); + }); + // ── Agent registry CRUD ───────────────────────────────────────────────── // Registry-side mutations only — the in-memory list configured at server // startup is never modified by these endpoints. Library-mode SDK consumers diff --git a/packages/engine-claude-agent-sdk/src/engine.test.ts b/packages/engine-claude-agent-sdk/src/engine.test.ts index 88e18ac..fb9a388 100644 --- a/packages/engine-claude-agent-sdk/src/engine.test.ts +++ b/packages/engine-claude-agent-sdk/src/engine.test.ts @@ -90,3 +90,84 @@ describe("ClaudeAgentEngine", () => { vi.doUnmock("@anthropic-ai/claude-agent-sdk"); }); }); + +describe("inheritEssentialHostEnv — Bedrock + POSIX passthrough", () => { + // The Claude Agent SDK runs as a subprocess of the harness; for it to find + // AWS Bedrock credentials via the standard SDK credential chain (IRSA token, + // shared creds, profile) those env vars must be inherited from the host. + // This regression test pins the allowlist so adding/removing entries is a + // conscious decision. + + it("propagates POSIX + XDG basics when set", async () => { + const { inheritEssentialHostEnv } = await import("./engine.js"); + const prev = { HOME: process.env.HOME, PATH: process.env.PATH, LANG: process.env.LANG }; + process.env.HOME = "/home/spike"; + process.env.PATH = "/spike/bin"; + process.env.LANG = "en_US.UTF-8"; + try { + const out = inheritEssentialHostEnv(); + expect(out.HOME).toBe("/home/spike"); + expect(out.PATH).toBe("/spike/bin"); + expect(out.LANG).toBe("en_US.UTF-8"); + } finally { + Object.assign(process.env, prev); + } + }); + + it("propagates the Bedrock + AWS IRSA env vars", async () => { + const { inheritEssentialHostEnv } = await import("./engine.js"); + const keys = [ + "CLAUDE_CODE_USE_BEDROCK", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEDROCK_MODEL_ID", + "AWS_ROLE_ARN", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_PROFILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_CONFIG_FILE", + ]; + const prev: Record = {}; + for (const k of keys) prev[k] = process.env[k]; + process.env.CLAUDE_CODE_USE_BEDROCK = "1"; + process.env.AWS_REGION = "us-west-2"; + process.env.AWS_DEFAULT_REGION = "us-east-1"; + process.env.AWS_BEDROCK_MODEL_ID = "anthropic.claude-sonnet-4-20250514-v1:0"; + process.env.AWS_ROLE_ARN = "arn:aws:iam::123456789012:role/spike"; + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"; + process.env.AWS_PROFILE = "default"; + process.env.AWS_SHARED_CREDENTIALS_FILE = "/etc/aws/credentials"; + process.env.AWS_CONFIG_FILE = "/etc/aws/config"; + try { + const out = inheritEssentialHostEnv(); + expect(out.CLAUDE_CODE_USE_BEDROCK).toBe("1"); + expect(out.AWS_REGION).toBe("us-west-2"); + expect(out.AWS_DEFAULT_REGION).toBe("us-east-1"); + expect(out.AWS_BEDROCK_MODEL_ID).toBe("anthropic.claude-sonnet-4-20250514-v1:0"); + expect(out.AWS_ROLE_ARN).toBe("arn:aws:iam::123456789012:role/spike"); + expect(out.AWS_WEB_IDENTITY_TOKEN_FILE).toBe( + "/var/run/secrets/eks.amazonaws.com/serviceaccount/token", + ); + expect(out.AWS_PROFILE).toBe("default"); + expect(out.AWS_SHARED_CREDENTIALS_FILE).toBe("/etc/aws/credentials"); + expect(out.AWS_CONFIG_FILE).toBe("/etc/aws/config"); + } finally { + for (const k of keys) { + if (prev[k] === undefined) delete process.env[k]; + else process.env[k] = prev[k]!; + } + } + }); + + it("omits keys that aren't set on the host (no empty-string leaks)", async () => { + const { inheritEssentialHostEnv } = await import("./engine.js"); + const prev = process.env.AWS_BEDROCK_MODEL_ID; + delete process.env.AWS_BEDROCK_MODEL_ID; + try { + const out = inheritEssentialHostEnv(); + expect("AWS_BEDROCK_MODEL_ID" in out).toBe(false); + } finally { + if (prev !== undefined) process.env.AWS_BEDROCK_MODEL_ID = prev; + } + }); +}); diff --git a/packages/engine-claude-agent-sdk/src/engine.ts b/packages/engine-claude-agent-sdk/src/engine.ts index db0656b..47cf2f9 100644 --- a/packages/engine-claude-agent-sdk/src/engine.ts +++ b/packages/engine-claude-agent-sdk/src/engine.ts @@ -261,7 +261,13 @@ function signalToController(signal: AbortSignal): AbortController { * * Caller envs (api keys, etc.) override these on conflict. */ -function inheritEssentialHostEnv(): Record { +/** + * Pure: snapshot the env vars the spawned harness subprocess needs from the + * parent process. Includes the standard POSIX/XDG essentials plus the AWS + * Bedrock envs the Claude Agent SDK reads when `CLAUDE_CODE_USE_BEDROCK=1`. + * Exported for testability — the engine itself calls it inline. + */ +export function inheritEssentialHostEnv(): Record { const out: Record = {}; for (const k of [ "HOME", @@ -273,6 +279,21 @@ function inheritEssentialHostEnv(): Record { "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", + // AWS Bedrock — when CLAUDE_CODE_USE_BEDROCK=1, the Claude Agent SDK + // switches transport to Bedrock and uses the standard AWS credential + // chain. On EKS that means the IRSA-projected web-identity token at + // AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN; locally it's the usual + // AWS_PROFILE / shared-credentials flow. Pass these through so the + // chain can find them inside the spawned harness subprocess. + "CLAUDE_CODE_USE_BEDROCK", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEDROCK_MODEL_ID", + "AWS_ROLE_ARN", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_PROFILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_CONFIG_FILE", ]) { const v = process.env[k]; if (v) out[k] = v; From 2545e9e380521f25169485796379780009154b57 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 05:50:26 -0400 Subject: [PATCH 4/9] =?UTF-8?q?test(sdk):=20Phase=202c=20=E2=80=94=20telem?= =?UTF-8?q?etry-hook=20+=20claude-sdk=20=C3=97=20substrate=20=C3=97=20sour?= =?UTF-8?q?ce=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new SDK test files + a shared GAP fixture, ~500 LOC. packages/sdk/src/telemetry-hook.test.ts (7 tests, runs offline in CI): Verifies AgentTelemetry lifecycle hooks fire correctly using a recording mock telemetry impl + MockEngine + in-process Hono harness: - onAgentConstructed fires with source/harness/model. - onChatStart → onChatEnd context threading on success path. - onChatEnd ok=false + error on failure path. - onClose fires on dispose(). - A throwing telemetry (sync + async) does NOT break the chat (safeFireTelemetry regression guard). - Agent without telemetry option still works (no-op branch coverage). packages/sdk/src/substrate-matrix.test.ts (10 tests, 8 env-gated): claude-agent-sdk × {Local, Bwrap, E2B} × {inline, local, git} matrix that actually calls Anthropic. Whole suite is ANTHROPIC_API_KEY-gated via describe.skipIf so CI offline is a no-op. Per-row guards: - Bwrap rows: skipIf(!Linux || !bwrap-on-PATH). - E2B rows: skipIf(!E2B_API_KEY). - Git rows: skipIf(!SDK_MATRIX_GIT_FIXTURE_URL). Each row boots the substrate, makes a real claude-agent-sdk chat (with a terse SOUL so the spend stays ~30 tokens / row), asserts non-zero output tokens, and disposes. Two fixture-shape tests run always to catch test-fixtures/ deletions. packages/sdk/test-fixtures/minimal-agent/{agent.yaml, SOUL.md}: Tiny GAP repo reused by all "local source" matrix rows (and matched by the inline source variant for source-type parity). packages/sdk/package.json: Add runtime-local / runtime-bwrap / runtime-e2b / engine-claude-agent-sdk / identity-gitagentprotocol to devDependencies so the matrix can import them dynamically (still workspace deps; never pulled by users). Verified: pnpm sdk typecheck clean; full SDK test suite 7 files / 62 tests (54 passed + 8 matrix rows auto-skipped offline). pnpm -r typecheck clean. --- packages/sdk/package.json | 5 + packages/sdk/src/substrate-matrix.test.ts | 233 +++++++++++++++ packages/sdk/src/telemetry-hook.test.ts | 270 ++++++++++++++++++ .../sdk/test-fixtures/minimal-agent/SOUL.md | 1 + .../test-fixtures/minimal-agent/agent.yaml | 6 + pnpm-lock.yaml | 15 + 6 files changed, 530 insertions(+) create mode 100644 packages/sdk/src/substrate-matrix.test.ts create mode 100644 packages/sdk/src/telemetry-hook.test.ts create mode 100644 packages/sdk/test-fixtures/minimal-agent/SOUL.md create mode 100644 packages/sdk/test-fixtures/minimal-agent/agent.yaml diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 24ff619..9e56118 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -27,6 +27,11 @@ }, "devDependencies": { "@computeragent/harness-server": "workspace:*", + "@computeragent/runtime-local": "workspace:*", + "@computeragent/runtime-bwrap": "workspace:*", + "@computeragent/runtime-e2b": "workspace:*", + "@computeragent/engine-claude-agent-sdk": "workspace:*", + "@computeragent/identity-gitagentprotocol": "workspace:*", "@computeragent/testing": "workspace:*", "@hono/node-server": "^1.13.0", "typescript": "^5.5.0", diff --git a/packages/sdk/src/substrate-matrix.test.ts b/packages/sdk/src/substrate-matrix.test.ts new file mode 100644 index 0000000..6c38c03 --- /dev/null +++ b/packages/sdk/src/substrate-matrix.test.ts @@ -0,0 +1,233 @@ +/** + * claude-agent-sdk × substrate × source-type matrix. + * + * The customer (Nordstrom) is "especially looking at Claude" — this suite + * proves the `claude-agent-sdk` engine works end-to-end across every + * substrate (Local / Bwrap / E2B) and every IdentitySource type (inline / + * local / git) they'd realistically use. + * + * Everything here makes REAL Anthropic API calls, so the whole suite is + * gated on `ANTHROPIC_API_KEY`. Per-row gates handle platform-specific + * substrates: Bwrap requires Linux + the `bwrap` binary; E2B requires + * `E2B_API_KEY`. Git-source tests need a tiny public GAP repo to clone — + * gated on `SDK_MATRIX_GIT_FIXTURE_URL` (or the default sentinel). + * + * Each row asserts: substrate boots, agent.chat() returns a ChatResult with + * non-zero output tokens, dispose() is clean. Token cost is intentionally + * tiny — the SOUL says "reply in fewer than 10 words" so each test is ~30 + * tokens of output × 9–15 rows × however many devs run it. Cheap enough. + * + * Skip semantics: + * - `describe.skip` when ANTHROPIC_API_KEY missing → whole file no-op in CI. + * - `it.skipIf` per row → row no-ops on missing creds / wrong platform. + * + * To run the full matrix locally: + * ANTHROPIC_API_KEY=sk-ant-... E2B_API_KEY=e2b_... \ + * pnpm --filter @computeragent/sdk exec vitest run src/substrate-matrix.test.ts + */ +import { existsSync } from "node:fs"; +import { resolve as resolvePath } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { ComputerAgent } from "./computer-agent.js"; +import type { ComputerAgentOptions } from "./types.js"; +import type { Substrate } from "./substrate.js"; +import type { IdentitySource } from "@computeragent/protocol"; + +const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; +const E2B_KEY = process.env.E2B_API_KEY ?? ""; +const GIT_FIXTURE_URL = process.env.SDK_MATRIX_GIT_FIXTURE_URL ?? ""; + +const IS_LINUX = process.platform === "linux"; +const HAS_BWRAP = (() => { + try { + // resolve via PATH — synchronous lookup ok in test bootstrap + const { execSync } = require("node:child_process") as typeof import("node:child_process"); + execSync("command -v bwrap", { stdio: "ignore" }); + return true; + } catch { + return false; + } +})(); + +/** Path to the in-repo fixture used by the "local source" tests. */ +const FIXTURE_DIR = resolvePath( + fileURLToPath(new URL(".", import.meta.url)), + "../test-fixtures/minimal-agent", +); + +/** Inline GAP manifest equivalent to the fixture dir — exercises the + * inline source path without touching disk. */ +const INLINE_SOURCE: IdentitySource = { + type: "inline", + manifest: { name: "minimal-agent", version: "0.1.0" }, + files: { + "agent.yaml": [ + 'spec_version: "0.1.0"', + "name: minimal-agent", + "version: 0.1.0", + "model:", + " preferred: claude-haiku-4-5-20251001", + ].join("\n"), + "SOUL.md": "You are a terse test agent. Reply in fewer than 10 words. Never use tools.", + }, +}; + +const LOCAL_SOURCE: IdentitySource = { type: "local", path: FIXTURE_DIR }; + +const GIT_SOURCE: IdentitySource | null = GIT_FIXTURE_URL + ? { type: "git", url: GIT_FIXTURE_URL } + : null; + +const HARNESS_NAME = "claude-agent-sdk" as const; +const TEST_MESSAGE = "Reply in exactly three words."; +const PER_RUN_TIMEOUT_MS = 90_000; // model latency + substrate boot + +/** + * Spawn-a-substrate factory so we can parametrize cleanly without coupling + * the test file to substrate constructors at module load time (Bwrap and E2B + * import are heavy + platform-conditional). + */ +async function makeSubstrate(kind: "local" | "bwrap" | "e2b"): Promise { + if (kind === "local") { + const { LocalSubstrate } = await import("@computeragent/runtime-local"); + return new LocalSubstrate(); + } + if (kind === "bwrap") { + const { BwrapSubstrate } = await import("@computeragent/runtime-bwrap"); + return new BwrapSubstrate(); + } + const { E2BSubstrate } = await import("@computeragent/runtime-e2b"); + return new E2BSubstrate({ apiKey: E2B_KEY }); +} + +/** One assertion path used by every row. Built so the matrix bodies stay tiny. */ +async function runOneRow( + kind: "local" | "bwrap" | "e2b", + source: IdentitySource, +): Promise<{ agent: ComputerAgent; outputTokens: number }> { + const substrate = await makeSubstrate(kind); + const opts: ComputerAgentOptions = { + source, + harness: HARNESS_NAME, + runtime: substrate, + envs: { ANTHROPIC_API_KEY: ANTHROPIC_KEY }, + options: { permissionMode: "bypassPermissions" }, + }; + const agent = new ComputerAgent(opts); + const result = await agent.chat(TEST_MESSAGE); + expect(result.sessionId).toBeTruthy(); + expect(result.messages.length).toBeGreaterThan(0); + expect(result.usage.outputTokens).toBeGreaterThan(0); + return { agent, outputTokens: result.usage.outputTokens }; +} + +describe.skipIf(!ANTHROPIC_KEY)( + "claude-agent-sdk × substrate × source matrix", + () => { + let activeAgent: ComputerAgent | undefined; + + afterEach(async () => { + if (activeAgent) { + await activeAgent.dispose().catch(() => {}); + activeAgent = undefined; + } + }); + + // ── LocalSubstrate row ───────────────────────────────────────────────── + it( + "Local × inline — boot, chat, dispose", + async () => { + const { agent } = await runOneRow("local", INLINE_SOURCE); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + it( + "Local × local-fixture — boot, chat, dispose", + async () => { + if (!existsSync(FIXTURE_DIR)) { + throw new Error(`fixture missing: ${FIXTURE_DIR}`); + } + const { agent } = await runOneRow("local", LOCAL_SOURCE); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + it.skipIf(!GIT_SOURCE)( + "Local × git — boot, clone, chat, dispose (SDK_MATRIX_GIT_FIXTURE_URL gated)", + async () => { + const { agent } = await runOneRow("local", GIT_SOURCE!); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + // ── BwrapSubstrate row (Linux only) ──────────────────────────────────── + it.skipIf(!IS_LINUX || !HAS_BWRAP)( + "Bwrap × inline — boot inside bwrap, chat, dispose", + async () => { + const { agent } = await runOneRow("bwrap", INLINE_SOURCE); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + it.skipIf(!IS_LINUX || !HAS_BWRAP)( + "Bwrap × local-fixture — boot inside bwrap, chat, dispose", + async () => { + const { agent } = await runOneRow("bwrap", LOCAL_SOURCE); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + it.skipIf(!IS_LINUX || !HAS_BWRAP || !GIT_SOURCE)( + "Bwrap × git — boot, clone, chat, dispose (SDK_MATRIX_GIT_FIXTURE_URL gated)", + async () => { + const { agent } = await runOneRow("bwrap", GIT_SOURCE!); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS, + ); + + // ── E2BSubstrate row (E2B_API_KEY required) ──────────────────────────── + it.skipIf(!E2B_KEY)( + "E2B × inline — sandbox boot, chat, dispose", + async () => { + const { agent } = await runOneRow("e2b", INLINE_SOURCE); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS * 2, // E2B boot is slower + ); + + it.skipIf(!E2B_KEY || !GIT_SOURCE)( + "E2B × git — sandbox boot, clone, chat, dispose (SDK_MATRIX_GIT_FIXTURE_URL gated)", + async () => { + const { agent } = await runOneRow("e2b", GIT_SOURCE!); + activeAgent = agent; + }, + PER_RUN_TIMEOUT_MS * 2, + ); + }, +); + +// Sanity test that always runs — confirms the fixture exists and is shaped +// correctly. Catches the case where someone deletes test-fixtures/ by accident. +describe("substrate-matrix fixture", () => { + it("local fixture has agent.yaml + SOUL.md", () => { + expect(existsSync(resolvePath(FIXTURE_DIR, "agent.yaml"))).toBe(true); + expect(existsSync(resolvePath(FIXTURE_DIR, "SOUL.md"))).toBe(true); + }); + + it("inline source is well-formed", () => { + expect(INLINE_SOURCE.type).toBe("inline"); + if (INLINE_SOURCE.type === "inline") { + expect(INLINE_SOURCE.manifest.name).toBe("minimal-agent"); + expect(INLINE_SOURCE.files?.["agent.yaml"]).toBeTruthy(); + expect(INLINE_SOURCE.files?.["SOUL.md"]).toBeTruthy(); + } + }); +}); diff --git a/packages/sdk/src/telemetry-hook.test.ts b/packages/sdk/src/telemetry-hook.test.ts new file mode 100644 index 0000000..ea0cc0b --- /dev/null +++ b/packages/sdk/src/telemetry-hook.test.ts @@ -0,0 +1,270 @@ +/** + * Verifies the AgentTelemetry lifecycle hooks fire correctly from the SDK. + * Uses MockEngine + an in-process harness server (the same pattern as + * computer-agent.test.ts) so this runs in CI offline — no Anthropic, no + * Mongo. A throwing telemetry impl must NOT break a chat (safeFireTelemetry). + */ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createHarnessServer } from "@computeragent/harness-server"; +import { MockEngine, MockLoader } from "@computeragent/testing"; +import { ComputerAgent } from "./computer-agent.js"; +import type { + AgentConstructedInfo, + AgentTelemetry, + ChatEndInfo, + ChatStartInfo, +} from "./telemetry.js"; + +interface RecordingTelemetry extends AgentTelemetry { + readonly events: Array<{ kind: string; info?: unknown; returnedCtx?: unknown }>; +} + +function makeRecorder(): RecordingTelemetry { + const events: RecordingTelemetry["events"] = []; + return { + events, + onAgentConstructed(info: AgentConstructedInfo) { + events.push({ kind: "constructed", info }); + }, + onChatStart(info: ChatStartInfo) { + const ctx = { startedAt: Date.now(), msg: info.message }; + events.push({ kind: "start", info, returnedCtx: ctx }); + return ctx; + }, + onChatEnd(info: ChatEndInfo) { + events.push({ kind: "end", info }); + }, + onClose() { + events.push({ kind: "close" }); + }, + }; +} + +let serverHandle: { stop: () => void; url: string } | undefined; + +async function bootServer(engine: MockEngine) { + const { serve } = await import("@hono/node-server"); + const app = createHarnessServer({ + engines: { mock: engine }, + identityLoaders: { + mock: new MockLoader({ metadata: { name: "test-agent", version: "0.1.0" } }), + }, + }); + return await new Promise<{ stop: () => void; url: string }>((resolve) => { + const server = serve({ fetch: app.fetch, port: 0 }, ({ port }) => { + resolve({ stop: () => server.close(), url: `http://127.0.0.1:${port}` }); + }); + }); +} + +beforeEach(() => { + serverHandle = undefined; +}); + +afterEach(() => { + serverHandle?.stop(); +}); + +describe("AgentTelemetry — lifecycle hooks fire", () => { + it("fires onAgentConstructed once with source/harness/model", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + const telemetry = makeRecorder(); + + const _agent = new ComputerAgent({ + source: { type: "local", path: "/tmp/spike" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + model: "claude-haiku-4-5", + telemetry, + }); + + // onAgentConstructed fires synchronously from the constructor (fire-and- + // forget), so by the next microtask tick the event is recorded. + await new Promise((r) => setTimeout(r, 5)); + const constructed = telemetry.events.find((e) => e.kind === "constructed"); + expect(constructed).toBeDefined(); + const info = constructed!.info as AgentConstructedInfo; + expect(info.harness).toBe("mock"); + expect(info.model).toBe("claude-haiku-4-5"); + expect(info.source).toEqual({ type: "local", path: "/tmp/spike" }); + }); + + it("fires onChatStart → onChatEnd with the context threaded between them (success path)", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "assistant", text: "hi" } }, + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + const telemetry = makeRecorder(); + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + telemetry, + }); + + await agent.chat("hello world"); + // Telemetry is fire-and-forget — flush microtasks so onChatEnd lands. + await new Promise((r) => setTimeout(r, 20)); + + const start = telemetry.events.find((e) => e.kind === "start"); + const end = telemetry.events.find((e) => e.kind === "end"); + expect(start).toBeDefined(); + expect(end).toBeDefined(); + + const startInfo = start!.info as ChatStartInfo; + expect(startInfo.message).toBe("hello world"); + expect(startInfo.sessionIdPromise).toBeInstanceOf(Promise); + + const endInfo = end!.info as ChatEndInfo; + expect(endInfo.ok).toBe(true); + expect(endInfo.context).toEqual(start!.returnedCtx); + expect(endInfo.sessionId).toMatch(/^sess_/); + expect(typeof endInfo.durationMs).toBe("number"); + expect(endInfo.durationMs).toBeGreaterThanOrEqual(0); + }); + + it("fires onChatEnd with ok=false + error when the chat fails", async () => { + // No harness URL = the SDK's first /v1/sessions call will fail with a + // connect error, which surfaces through ChatHandle.then's reject branch. + const telemetry = makeRecorder(); + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: "http://127.0.0.1:1", // port 1 — guaranteed connect refused + telemetry, + }); + + await expect(agent.chat("doomed")).rejects.toBeDefined(); + await new Promise((r) => setTimeout(r, 30)); + + const end = telemetry.events.find((e) => e.kind === "end"); + expect(end).toBeDefined(); + const endInfo = end!.info as ChatEndInfo; + expect(endInfo.ok).toBe(false); + expect(endInfo.error).toBeDefined(); + expect(typeof endInfo.error).toBe("string"); + }); + + it("fires onClose on dispose()", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + const telemetry = makeRecorder(); + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + telemetry, + }); + + await agent.dispose(); + await new Promise((r) => setTimeout(r, 10)); + + expect(telemetry.events.some((e) => e.kind === "close")).toBe(true); + }); + + it("a throwing telemetry impl does NOT break a chat (safeFireTelemetry)", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "assistant", text: "ok" } }, + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + + const throwing: AgentTelemetry = { + onAgentConstructed() { + throw new Error("boom in constructed"); + }, + onChatStart() { + throw new Error("boom in start"); + }, + onChatEnd() { + throw new Error("boom in end"); + }, + onClose() { + throw new Error("boom in close"); + }, + }; + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + telemetry: throwing, + }); + + // The chat must succeed despite every telemetry hook throwing. + const result = await agent.chat("survive"); + expect(result.sessionId).toMatch(/^sess_/); + + await agent.dispose(); + // No exception should have propagated. + }); + + it("an async-rejecting telemetry impl also doesn't break the chat", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + + const rejecting: AgentTelemetry = { + onAgentConstructed: async () => { + throw new Error("async boom constructed"); + }, + onChatStart: () => undefined, + onChatEnd: async () => { + throw new Error("async boom end"); + }, + onClose: async () => { + throw new Error("async boom close"); + }, + }; + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + telemetry: rejecting, + }); + + const result = await agent.chat("survive async"); + expect(result.sessionId).toMatch(/^sess_/); + await agent.dispose(); + // No unhandled rejection should crash the test runner. + }); + + it("agent without a telemetry option works exactly as before (no hooks fire)", async () => { + const engine = new MockEngine([ + { kind: "emit", payload: { type: "result", text: "done" } }, + ]); + serverHandle = await bootServer(engine); + + const agent = new ComputerAgent({ + source: { type: "local", path: "/tmp" }, + harness: "mock", + identityLoader: "mock", + harnessUrl: serverHandle.url, + // no telemetry + }); + + const result = await agent.chat("plain"); + expect(result.sessionId).toMatch(/^sess_/); + await agent.dispose(); + // Nothing to assert on the (absent) telemetry — the test passes as long + // as construction + chat + dispose don't error. Regression guard for the + // hook-wiring branches. + }); +}); diff --git a/packages/sdk/test-fixtures/minimal-agent/SOUL.md b/packages/sdk/test-fixtures/minimal-agent/SOUL.md new file mode 100644 index 0000000..37c20de --- /dev/null +++ b/packages/sdk/test-fixtures/minimal-agent/SOUL.md @@ -0,0 +1 @@ +You are a terse test agent. Reply in fewer than 10 words. Never use tools. diff --git a/packages/sdk/test-fixtures/minimal-agent/agent.yaml b/packages/sdk/test-fixtures/minimal-agent/agent.yaml new file mode 100644 index 0000000..7d02cc8 --- /dev/null +++ b/packages/sdk/test-fixtures/minimal-agent/agent.yaml @@ -0,0 +1,6 @@ +spec_version: "0.1.0" +name: minimal-agent +version: 0.1.0 +description: "Tiny GAP agent used by the substrate × source matrix tests." +model: + preferred: claude-haiku-4-5-20251001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9a39e5..8eb1990 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -482,9 +482,24 @@ importers: specifier: workspace:* version: link:../protocol devDependencies: + '@computeragent/engine-claude-agent-sdk': + specifier: workspace:* + version: link:../engine-claude-agent-sdk '@computeragent/harness-server': specifier: workspace:* version: link:../harness-server + '@computeragent/identity-gitagentprotocol': + specifier: workspace:* + version: link:../identity-gitagentprotocol + '@computeragent/runtime-bwrap': + specifier: workspace:* + version: link:../runtime-bwrap + '@computeragent/runtime-e2b': + specifier: workspace:* + version: link:../runtime-e2b + '@computeragent/runtime-local': + specifier: workspace:* + version: link:../runtime-local '@computeragent/testing': specifier: workspace:* version: link:../testing From 981a4f934eb02db821197da058651f9fbe103814 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 06:29:16 -0400 Subject: [PATCH 5/9] =?UTF-8?q?test:=20TDD=20backfill=20=E2=80=94=20agent-?= =?UTF-8?q?registry-mongo=20+=20agentos=20CRUD=20+=20displaySource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the test coverage that was missing from Phases 1 / 2b / 2c shipping without it. Total: 67 new tests across 4 files, 1163 LOC. - packages/agent-registry-mongo/src/registry.test.ts (9 tests): upsert shape, idempotent registeredAt, list ordering, get null, unregister idempotency, close idempotency, shared MongoClient. - packages/agent-registry-mongo/src/audit-log.test.ts (8 tests): append shape, QUERY_MAX/REPLY_MAX truncation with … suffix, newest-first + limit clamping [1, 500], filter combos (source/ok/before), count() vs list() equivalence. - packages/agent-registry-mongo/src/telemetry.test.ts (12 tests): onAgentConstructed upserts via SDK info; falls back to ctor-supplied agent fields; onChatStart returns ctx; onChatEnd appends success + failure rows with usage breakouts; durationMs derives from ctx when SDK omits it; configurable source tag; onError fires on Mongo failure without throwing; shared client lifecycle. - examples/agentos-api.test.ts (18 tests): drives createAgentOSApp via app.fetch(Request). POST /register (400 on missing name, upsert shape, idempotent registeredAt), PATCH (409 on in-memory, 404 on unknown, field update), DELETE (409/404/200), GET /by-source (400/in-memory match/registry match/dual match/empty 200), GET /agents (union, origin tagging, in-memory wins). - agentos/src/api.test.ts (20 tests): pure-function tests for displaySource() — null/undefined sentinels, local + inline structured, git URL parsing (https, ssh git@, with ref, ref URL-encoded), recognized hosts (github/gitlab/bitbucket), scheme-less + bare owner/repo, legacy string source, fallbacks. Live-Mongo paths use the same `describeMongo = url ? describe : describe.skip` gate the session-store-mongo tests already use; unique DB per run keeps parallel runs isolated. Offline (no MONGO_URL): 4 always-on tests pass, 57 env-gated skip cleanly. Workspace-wide pnpm -r test stays green. Wires vitest into examples/ and agentos/ (test script + ^2.0.0 devDep) so the suite is invokable per-package. --- agentos/package.json | 7 +- agentos/src/api.test.ts | 197 +++++++++++ examples/agentos-api.test.ts | 308 ++++++++++++++++++ examples/package.json | 6 +- .../src/audit-log.test.ts | 182 +++++++++++ .../agent-registry-mongo/src/registry.test.ts | 166 ++++++++++ .../src/telemetry.test.ts | 298 +++++++++++++++++ pnpm-lock.yaml | 3 + 8 files changed, 1163 insertions(+), 4 deletions(-) create mode 100644 agentos/src/api.test.ts create mode 100644 examples/agentos-api.test.ts create mode 100644 packages/agent-registry-mongo/src/audit-log.test.ts create mode 100644 packages/agent-registry-mongo/src/registry.test.ts create mode 100644 packages/agent-registry-mongo/src/telemetry.test.ts diff --git a/agentos/package.json b/agentos/package.json index fc74d65..2f07be5 100644 --- a/agentos/package.json +++ b/agentos/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc -b --noEmit" }, "dependencies": { "react": "^18.3.1", @@ -20,6 +22,7 @@ "postcss": "^8.4.49", "tailwindcss": "^3.4.17", "typescript": "^5.7.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vitest": "^2.0.0" } } diff --git a/agentos/src/api.test.ts b/agentos/src/api.test.ts new file mode 100644 index 0000000..57d450b --- /dev/null +++ b/agentos/src/api.test.ts @@ -0,0 +1,197 @@ +/** + * Pure-function unit tests for `displaySource`. No DOM, no fetch, no React. + * + * displaySource is the load-bearing piece behind in the agent + * rail: it turns the variable-shaped `agent.source` (git/local/inline object + * OR a legacy bare string) into a render-ready `{kind, primary, secondary, + * href?}` triple. Wrong output here = the dashboard renders the wrong title + * or links to the wrong repo, so we exercise every recognized shape + every + * fallback branch. + */ +import { describe, expect, it } from "vitest"; +import { displaySource } from "./api.js"; + +describe("displaySource", () => { + describe("null / undefined input", () => { + it("returns the (no source) unknown sentinel for undefined", () => { + expect(displaySource(undefined)).toEqual({ + kind: "unknown", + primary: "(no source)", + secondary: "", + }); + }); + it("returns the (no source) unknown sentinel for null", () => { + expect(displaySource(null)).toEqual({ + kind: "unknown", + primary: "(no source)", + secondary: "", + }); + }); + it("returns the (no source) unknown sentinel for empty string", () => { + expect(displaySource("")).toEqual({ + kind: "unknown", + primary: "(no source)", + secondary: "", + }); + }); + }); + + describe("structured local source", () => { + it("uses the last two path segments as the primary label", () => { + expect(displaySource({ type: "local", path: "/Users/zeus/repos/devsupport-agent" })).toEqual({ + kind: "local", + primary: "repos/devsupport-agent", + secondary: "/Users/zeus/repos/devsupport-agent", + }); + }); + it("falls back to the full path when there's only one segment", () => { + expect(displaySource({ type: "local", path: "/agent" })).toEqual({ + kind: "local", + primary: "agent", + secondary: "/agent", + }); + }); + it("handles a trailing slash without producing an empty primary", () => { + const out = displaySource({ type: "local", path: "/Users/zeus/x/" }); + expect(out.kind).toBe("local"); + expect(out.primary.length).toBeGreaterThan(0); + }); + }); + + describe("structured inline source", () => { + it("uses manifest.name when present", () => { + expect( + displaySource({ type: "inline", manifest: { name: "ad-hoc-bot" } }), + ).toEqual({ kind: "inline", primary: "ad-hoc-bot", secondary: "inline manifest" }); + }); + it("falls back to 'inline' when manifest has no name", () => { + expect(displaySource({ type: "inline", manifest: { spec_version: "0.1.0" } })).toEqual({ + kind: "inline", + primary: "inline", + secondary: "inline manifest", + }); + }); + it("falls back to 'inline' when manifest.name is not a string", () => { + expect( + displaySource({ + type: "inline", + manifest: { name: 42 as unknown as string }, + }), + ).toEqual({ kind: "inline", primary: "inline", secondary: "inline manifest" }); + }); + }); + + describe("structured git source", () => { + it("parses an https URL with .git suffix into owner/repo + host + href", () => { + expect( + displaySource({ + type: "git", + url: "https://github.com/open-gitagent/ComputerAgent.git", + }), + ).toEqual({ + kind: "git", + primary: "open-gitagent/ComputerAgent", + secondary: "github.com", + href: "https://github.com/open-gitagent/ComputerAgent", + }); + }); + it("parses an ssh git@ URL into owner/repo + host + https href", () => { + expect( + displaySource({ type: "git", url: "git@github.com:open-gitagent/opengap.git" }), + ).toEqual({ + kind: "git", + primary: "open-gitagent/opengap", + secondary: "github.com", + href: "https://github.com/open-gitagent/opengap", + }); + }); + it("appends /tree/ to the href when ref is provided", () => { + expect( + displaySource({ + type: "git", + url: "https://github.com/open-gitagent/ComputerAgent", + ref: "main", + }), + ).toEqual({ + kind: "git", + primary: "open-gitagent/ComputerAgent", + secondary: "github.com", + href: "https://github.com/open-gitagent/ComputerAgent/tree/main", + }); + }); + it("url-encodes a ref containing a slash", () => { + const out = displaySource({ + type: "git", + url: "https://github.com/o/r", + ref: "feat/abc", + }); + expect(out.href).toBe("https://github.com/o/r/tree/feat%2Fabc"); + }); + it("recognizes gitlab.com hosts", () => { + expect( + displaySource({ type: "git", url: "https://gitlab.com/my-org/my-repo" }), + ).toEqual({ + kind: "git", + primary: "my-org/my-repo", + secondary: "gitlab.com", + href: "https://gitlab.com/my-org/my-repo", + }); + }); + it("recognizes bitbucket.org hosts", () => { + expect( + displaySource({ type: "git", url: "https://bitbucket.org/team/proj.git" }), + ).toEqual({ + kind: "git", + primary: "team/proj", + secondary: "bitbucket.org", + href: "https://bitbucket.org/team/proj", + }); + }); + it("parses a scheme-less host/owner/repo", () => { + expect(displaySource({ type: "git", url: "github.com/o/r" })).toEqual({ + kind: "git", + primary: "o/r", + secondary: "github.com", + href: "https://github.com/o/r", + }); + }); + it("treats a bare owner/repo (no host) as github by default", () => { + expect(displaySource({ type: "git", url: "open-gitagent/opengap" })).toEqual({ + kind: "git", + primary: "open-gitagent/opengap", + secondary: "github.com", + href: "https://github.com/open-gitagent/opengap", + }); + }); + }); + + describe("legacy string source", () => { + it("treats a full https URL the same as the structured form", () => { + const expected = { + kind: "git" as const, + primary: "open-gitagent/ComputerAgent", + secondary: "github.com", + href: "https://github.com/open-gitagent/ComputerAgent", + }; + expect(displaySource("https://github.com/open-gitagent/ComputerAgent")).toEqual(expected); + }); + it("treats a bare owner/repo string as github", () => { + expect(displaySource("open-gitagent/opengap")).toEqual({ + kind: "git", + primary: "open-gitagent/opengap", + secondary: "github.com", + href: "https://github.com/open-gitagent/opengap", + }); + }); + }); + + describe("unrecognized shapes fall back cleanly", () => { + it("returns kind=unknown with raw primary when there's only one path segment", () => { + expect(displaySource("just-a-name")).toEqual({ + kind: "unknown", + primary: "just-a-name", + secondary: "", + }); + }); + }); +}); diff --git a/examples/agentos-api.test.ts b/examples/agentos-api.test.ts new file mode 100644 index 0000000..0ef27ac --- /dev/null +++ b/examples/agentos-api.test.ts @@ -0,0 +1,308 @@ +/** + * Hono-level integration tests for `createAgentOSApp` CRUD routes: + * GET /agentos/api/agents (union of in-memory + registry) + * GET /agentos/api/agents/by-source (lookup by canonical URL) + * POST /agentos/api/agents/register (upsert into agent_registry) + * PATCH /agentos/api/agents/:name (update — not in-memory) + * DELETE /agentos/api/agents/:name (remove — not in-memory) + * + * Drives the app via `app.fetch(new Request(...))` — no real HTTP server. + * Live-Mongo gated on MONGO_URL; unique DB per run for isolation. + * + * What we don't cover here: the loopback `caBase` fetch in GET /agents (it + * decorates the response with active-sandbox state). The handler swallows + * fetch errors as best-effort, so the test paths still return clean shapes + * without that fetch ever succeeding. + */ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { MongoClient } from "mongodb"; +import { createAgentOSApp, type AgentDef } from "./agentos-api.ts"; +import { AgentLogStore } from "./agent-log-store.ts"; + +const url = process.env.MONGO_URL; +const describeMongo = url ? describe : describe.skip; + +describeMongo("agentos-api CRUD (live)", () => { + let admin: MongoClient; + let dbName: string; + let logStore: AgentLogStore; + let app: ReturnType; + + const IN_MEMORY_AGENTS: readonly AgentDef[] = [ + { + name: "gitagent", + label: "GitAgent", + harness: "gitagent", + source: "github.com/open-gitagent/opengap", + }, + { + name: "claude-code", + label: "Claude Code", + harness: "claude-agent-sdk", + source: "github.com/anthropics/claude-code", + }, + ]; + + beforeAll(async () => { + admin = new MongoClient(url!); + await admin.connect(); + dbName = `ca_test_agentosapi_${Math.random().toString(36).slice(2, 10)}`; + logStore = new AgentLogStore(url!, dbName); + app = createAgentOSApp({ + mongoUrl: url!, + mongoDb: dbName, + agents: IN_MEMORY_AGENTS, + logStore, + // No scheduleStore — schedule endpoints aren't under test here. + }); + }); + + afterAll(async () => { + await logStore.close?.().catch(() => {}); + await admin.db(dbName).dropDatabase().catch(() => {}); + await admin.close(); + }); + + afterEach(async () => { + await admin.db(dbName).collection("agent_registry").deleteMany({}); + }); + + const req = (method: string, path: string, body?: unknown) => + app.fetch( + new Request(`http://test.local${path}`, { + method, + headers: body !== undefined ? { "content-type": "application/json" } : undefined, + body: body !== undefined ? JSON.stringify(body) : undefined, + }), + ); + + describe("POST /agents/register", () => { + it("400s when name is missing", async () => { + const r = await req("POST", "/agentos/api/agents/register", { label: "x" }); + expect(r.status).toBe(400); + const j = (await r.json()) as { error: { code: string } }; + expect(j.error.code).toBe("BAD_REQUEST"); + }); + + it("upserts a new agent into agent_registry", async () => { + const r = await req("POST", "/agentos/api/agents/register", { + name: "library-agent", + label: "Library Agent", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/lib" }, + model: "bedrock/claude-sonnet-4", + registeredBy: "test-host", + }); + expect(r.status).toBe(200); + const doc = await admin.db(dbName).collection("agent_registry").findOne({ _id: "library-agent" }); + expect(doc).not.toBeNull(); + expect(doc!.label).toBe("Library Agent"); + expect(doc!.harness).toBe("claude-agent-sdk"); + expect(doc!.source).toEqual({ type: "git", url: "github.com/o/lib" }); + expect(doc!.model).toBe("bedrock/claude-sonnet-4"); + expect(doc!.registeredBy).toBe("test-host"); + expect(doc!.registeredAt).toBeInstanceOf(Date); + expect(doc!.updatedAt).toBeInstanceOf(Date); + }); + + it("is idempotent — re-registering preserves registeredAt", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "idempotent-agent", + harness: "gitagent", + source: "github.com/o/idemp", + }); + const firstDoc = await admin.db(dbName).collection("agent_registry").findOne({ _id: "idempotent-agent" }); + const firstAt = firstDoc!.registeredAt!.getTime(); + + await new Promise((r) => setTimeout(r, 10)); + await req("POST", "/agentos/api/agents/register", { + name: "idempotent-agent", + harness: "gitagent", + source: "github.com/o/idemp", + label: "Now Labeled", + }); + const secondDoc = await admin.db(dbName).collection("agent_registry").findOne({ _id: "idempotent-agent" }); + expect(secondDoc!.registeredAt!.getTime()).toBe(firstAt); + expect(secondDoc!.label).toBe("Now Labeled"); + }); + }); + + describe("PATCH /agents/:name", () => { + it("409s when the target is an in-memory agent", async () => { + const r = await req("PATCH", "/agentos/api/agents/gitagent", { label: "no" }); + expect(r.status).toBe(409); + const j = (await r.json()) as { error: { code: string } }; + expect(j.error.code).toBe("IN_MEMORY_AGENT"); + }); + + it("404s when the registry has no such agent", async () => { + const r = await req("PATCH", "/agentos/api/agents/never-registered", { label: "nope" }); + expect(r.status).toBe(404); + }); + + it("updates label/harness/source/model in the registry", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "patchable", + harness: "gitagent", + source: "github.com/o/p", + label: "before", + }); + const r = await req("PATCH", "/agentos/api/agents/patchable", { + label: "after", + model: "claude-haiku-4-5", + }); + expect(r.status).toBe(200); + const doc = await admin.db(dbName).collection("agent_registry").findOne({ _id: "patchable" }); + expect(doc!.label).toBe("after"); + expect(doc!.model).toBe("claude-haiku-4-5"); + expect(doc!.harness).toBe("gitagent"); // untouched + }); + }); + + describe("DELETE /agents/:name", () => { + it("409s when the target is an in-memory agent", async () => { + const r = await req("DELETE", "/agentos/api/agents/gitagent"); + expect(r.status).toBe(409); + }); + + it("404s when the registry has no such agent", async () => { + const r = await req("DELETE", "/agentos/api/agents/missing-from-registry"); + expect(r.status).toBe(404); + }); + + it("removes a registry agent", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "doomed", + harness: "gitagent", + source: "github.com/o/d", + }); + const r = await req("DELETE", "/agentos/api/agents/doomed"); + expect(r.status).toBe(200); + const doc = await admin.db(dbName).collection("agent_registry").findOne({ _id: "doomed" }); + expect(doc).toBeNull(); + }); + }); + + describe("GET /agents/by-source", () => { + it("400s when `url` query param is missing", async () => { + const r = await req("GET", "/agentos/api/agents/by-source"); + expect(r.status).toBe(400); + }); + + it("finds an in-memory agent whose source string matches", async () => { + const r = await req( + "GET", + `/agentos/api/agents/by-source?url=${encodeURIComponent("github.com/open-gitagent/opengap")}`, + ); + expect(r.status).toBe(200); + const j = (await r.json()) as { matches: Array<{ name: string }> }; + expect(j.matches.map((m) => m.name)).toContain("gitagent"); + }); + + it("finds a registry agent whose structured git source matches", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "by-src-registry", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/by-src" }, + }); + const r = await req( + "GET", + `/agentos/api/agents/by-source?url=${encodeURIComponent("github.com/o/by-src")}`, + ); + expect(r.status).toBe(200); + const j = (await r.json()) as { matches: Array<{ name: string }> }; + expect(j.matches.map((m) => m.name)).toContain("by-src-registry"); + }); + + it("returns BOTH when the same URL is registered under two names (dedup detection)", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "dev-worker", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/shared" }, + }); + await req("POST", "/agentos/api/agents/register", { + name: "prod-worker", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/shared" }, + }); + const r = await req( + "GET", + `/agentos/api/agents/by-source?url=${encodeURIComponent("github.com/o/shared")}`, + ); + const j = (await r.json()) as { matches: Array<{ name: string }> }; + const names = j.matches.map((m) => m.name).sort(); + expect(names).toEqual(["dev-worker", "prod-worker"]); + }); + + it("returns an empty match list (200) when nothing matches", async () => { + const r = await req( + "GET", + `/agentos/api/agents/by-source?url=${encodeURIComponent("github.com/no/match")}`, + ); + expect(r.status).toBe(200); + const j = (await r.json()) as { matches: unknown[] }; + expect(j.matches).toEqual([]); + }); + }); + + describe("GET /agents (union)", () => { + it("returns just the in-memory agents when the registry is empty", async () => { + const r = await req("GET", "/agentos/api/agents"); + expect(r.status).toBe(200); + const j = (await r.json()) as { agents: Array<{ name: string; origin: string }> }; + const inMem = j.agents.filter((a) => a.origin === "in-memory").map((a) => a.name).sort(); + expect(inMem).toEqual(["claude-code", "gitagent"]); + expect(j.agents.filter((a) => a.origin === "registry")).toHaveLength(0); + }); + + it("unions in-memory + registry agents and tags origin correctly", async () => { + await req("POST", "/agentos/api/agents/register", { + name: "registry-only", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/ro" }, + }); + const r = await req("GET", "/agentos/api/agents"); + const j = (await r.json()) as { agents: Array<{ name: string; origin: string }> }; + const reg = j.agents.find((a) => a.name === "registry-only"); + expect(reg).toBeDefined(); + expect(reg!.origin).toBe("registry"); + const inMem = j.agents.find((a) => a.name === "gitagent"); + expect(inMem!.origin).toBe("in-memory"); + }); + + it("in-memory wins on name collision (registry row is hidden)", async () => { + // Register a row that shadows an in-memory name. + await req("POST", "/agentos/api/agents/register", { + name: "gitagent", + harness: "claude-agent-sdk", // bogus — different harness, would-be wrong + source: "github.com/wrong/wrong", + registeredBy: "should-not-win", + }); + const r = await req("GET", "/agentos/api/agents"); + const j = (await r.json()) as { agents: Array<{ name: string; origin: string; harness: string }> }; + const all = j.agents.filter((a) => a.name === "gitagent"); + expect(all).toHaveLength(1); + expect(all[0]!.origin).toBe("in-memory"); + expect(all[0]!.harness).toBe("gitagent"); // server-configured one wins + }); + }); +}); + +// Always-on sanity test: the factory builds an app without touching Mongo. +describe("createAgentOSApp — factory (offline)", () => { + it("returns a Hono app exposing /agentos/api/health", async () => { + const log = new AgentLogStore("mongodb://nope:27017", "x"); + const app = createAgentOSApp({ + mongoUrl: "mongodb://nope:27017", + mongoDb: "x", + agents: [], + logStore: log, + }); + const r = await app.fetch(new Request("http://test.local/agentos/api/health")); + expect(r.status).toBe(200); + const j = (await r.json()) as { ok: boolean; agents: string[] }; + expect(j.ok).toBe(true); + expect(j.agents).toEqual([]); + await log.close?.().catch(() => {}); + }); +}); diff --git a/examples/package.json b/examples/package.json index 8d49159..2e70098 100644 --- a/examples/package.json +++ b/examples/package.json @@ -11,7 +11,8 @@ "binary-artifact-demo": "bun run binary-artifact-demo.ts", "ppt-agent": "bun run ppt-agent.ts", "pdf-agent": "bun run pdf-agent.ts", - "hil-demo": "bun run hil-demo.ts" + "hil-demo": "bun run hil-demo.ts", + "test": "vitest run --passWithNoTests" }, "dependencies": { "computeragent": "workspace:*", @@ -35,6 +36,7 @@ "mongodb": "^6.10.0" }, "devDependencies": { - "@types/tar-stream": "^3.1.3" + "@types/tar-stream": "^3.1.3", + "vitest": "^2.0.0" } } diff --git a/packages/agent-registry-mongo/src/audit-log.test.ts b/packages/agent-registry-mongo/src/audit-log.test.ts new file mode 100644 index 0000000..c947fff --- /dev/null +++ b/packages/agent-registry-mongo/src/audit-log.test.ts @@ -0,0 +1,182 @@ +/** + * Live-Mongo integration tests for `AgentLogStore`. Same MONGO_URL gate + + * unique-DB-per-run pattern as registry.test.ts. + */ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { MongoClient } from "mongodb"; +import { AgentLogStore, QUERY_MAX, REPLY_MAX } from "./audit-log.js"; + +const url = process.env.MONGO_URL; +const describeMongo = url ? describe : describe.skip; + +describeMongo("AgentLogStore (live)", () => { + let admin: MongoClient | null = null; + let dbName: string; + + beforeAll(async () => { + admin = new MongoClient(url!); + await admin.connect(); + dbName = `ca_test_logs_${Math.random().toString(36).slice(2, 10)}`; + }); + + afterAll(async () => { + if (admin) { + await admin.db(dbName).dropDatabase().catch(() => {}); + await admin.close(); + } + }); + + let store: AgentLogStore; + afterEach(async () => { + if (store) await store.close(); + if (admin) await admin.db(dbName).collection("agent_logs").deleteMany({}); + }); + + it("append() inserts one doc with the right shape", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + await store.append({ + source: "library", + agentName: "agent-a", + requester: "library", + channel: null, + threadTs: null, + sessionId: "sess_abc", + query: "hello", + reply: "world", + ok: true, + durationMs: 1234, + inputTokens: 50, + outputTokens: 8, + costUsd: 0.001, + }); + const rows = await store.list({ agentName: "agent-a" }); + expect(rows).toHaveLength(1); + const row = rows[0]!; + expect(row._id).toMatch(/^log_/); + expect(row.ts).toBeInstanceOf(Date); + expect(row.source).toBe("library"); + expect(row.agentName).toBe("agent-a"); + expect(row.requester).toBe("library"); + expect(row.sessionId).toBe("sess_abc"); + expect(row.query).toBe("hello"); + expect(row.reply).toBe("world"); + expect(row.ok).toBe(true); + expect(row.durationMs).toBe(1234); + expect(row.inputTokens).toBe(50); + expect(row.outputTokens).toBe(8); + expect(row.costUsd).toBe(0.001); + }); + + it("append() truncates query and reply to their MAX bounds", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + const longQuery = "q".repeat(QUERY_MAX + 100); + const longReply = "r".repeat(REPLY_MAX + 100); + await store.append({ + source: "library", + agentName: "agent-trunc", + requester: null, + channel: null, + threadTs: null, + sessionId: null, + query: longQuery, + reply: longReply, + ok: true, + }); + const [row] = await store.list({ agentName: "agent-trunc" }); + expect(row!.query.length).toBe(QUERY_MAX); + expect(row!.query.endsWith("…")).toBe(true); + expect(row!.reply.length).toBe(REPLY_MAX); + expect(row!.reply.endsWith("…")).toBe(true); + }); + + it("list() returns newest first and respects limit", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + for (let i = 0; i < 5; i++) { + await store.append({ + source: "library", + agentName: "agent-order", + requester: null, + channel: null, + threadTs: null, + sessionId: `sess_${i}`, + query: `q${i}`, + reply: `r${i}`, + ok: true, + }); + await new Promise((r) => setTimeout(r, 5)); + } + const rows = await store.list({ agentName: "agent-order", limit: 3 }); + expect(rows).toHaveLength(3); + expect(rows.map((r) => r.sessionId)).toEqual(["sess_4", "sess_3", "sess_2"]); + }); + + it("list() clamps limit to [1, 500]", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + await store.append({ + source: "library", + agentName: "agent-clamp", + requester: null, + channel: null, + threadTs: null, + sessionId: null, + query: "q", + reply: "r", + ok: true, + }); + // limit 0 → clamped to 1 + expect((await store.list({ agentName: "agent-clamp", limit: 0 })).length).toBe(1); + // limit 9999 → clamped to 500 (allowed, no error) + expect((await store.list({ agentName: "agent-clamp", limit: 9999 })).length).toBe(1); + }); + + it("list() filters by source + ok + before", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + const before = new Date(); + // 3 rows from "library" + 1 from "slack", mix of ok/error + await store.append({ source: "library", agentName: "f", requester: null, channel: null, threadTs: null, sessionId: null, query: "a", reply: "", ok: true }); + await store.append({ source: "library", agentName: "f", requester: null, channel: null, threadTs: null, sessionId: null, query: "b", reply: "", ok: false, error: "boom" }); + await store.append({ source: "library", agentName: "f", requester: null, channel: null, threadTs: null, sessionId: null, query: "c", reply: "", ok: true }); + await store.append({ source: "slack", agentName: "f", requester: null, channel: null, threadTs: null, sessionId: null, query: "d", reply: "", ok: true }); + const after = new Date(); + + expect((await store.list({ agentName: "f", source: "library" })).length).toBe(3); + expect((await store.list({ agentName: "f", source: "slack" })).length).toBe(1); + expect((await store.list({ agentName: "f", ok: false })).length).toBe(1); + expect((await store.list({ agentName: "f", before })).length).toBe(0); + expect((await store.list({ agentName: "f", before: after })).length).toBeGreaterThan(0); + }); + + it("count() returns the same numeric total as a filtered list", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + for (let i = 0; i < 4; i++) { + await store.append({ + source: i % 2 === 0 ? "library" : "slack", + agentName: "agent-count", + requester: null, + channel: null, + threadTs: null, + sessionId: null, + query: "q", + reply: "r", + ok: true, + }); + } + expect(await store.count({ agentName: "agent-count" })).toBe(4); + expect(await store.count({ agentName: "agent-count", source: "library" })).toBe(2); + expect(await store.count({ agentName: "agent-count", source: "slack" })).toBe(2); + }); + + it("close() is idempotent and safe before any I/O", async () => { + store = new AgentLogStore({ url: url!, database: dbName }); + await store.close(); + await store.close(); + }); +}); + +// Always-on sanity — constructor doesn't connect eagerly. +describe("AgentLogStore — constructor (offline)", () => { + it("does not throw with a bogus URL", () => { + const s = new AgentLogStore({ url: "mongodb://nope:27017", database: "x" }); + expect(s).toBeDefined(); + }); +}); diff --git a/packages/agent-registry-mongo/src/registry.test.ts b/packages/agent-registry-mongo/src/registry.test.ts new file mode 100644 index 0000000..e4b0c68 --- /dev/null +++ b/packages/agent-registry-mongo/src/registry.test.ts @@ -0,0 +1,166 @@ +/** + * Live-Mongo integration tests for `AgentRegistry`. Mirrors the + * session-store-mongo pattern: auto-skip when MONGO_URL is absent, use a + * unique DB per test run so concurrent runs don't collide, drop the DB on + * teardown. + * + * MONGO_URL=mongodb://localhost:27017/admin pnpm test + */ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { MongoClient } from "mongodb"; +import { AgentRegistry } from "./registry.js"; + +const url = process.env.MONGO_URL; +const describeMongo = url ? describe : describe.skip; + +describeMongo("AgentRegistry (live)", () => { + let admin: MongoClient | null = null; + let dbName: string; + + beforeAll(async () => { + admin = new MongoClient(url!); + await admin.connect(); + dbName = `ca_test_registry_${Math.random().toString(36).slice(2, 10)}`; + }); + + afterAll(async () => { + if (admin) { + await admin.db(dbName).dropDatabase().catch(() => {}); + await admin.close(); + } + }); + + let registry: AgentRegistry; + afterEach(async () => { + if (registry) await registry.close(); + if (admin) await admin.db(dbName).collection("agent_registry").deleteMany({}); + }); + + it("register() upserts a doc by name with registeredAt/updatedAt/lastSeen", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.register({ + name: "agent-a", + label: "Agent A", + harness: "claude-agent-sdk", + source: { type: "git", url: "github.com/o/r" }, + model: "bedrock/claude-sonnet-4", + registeredBy: "test", + }); + const doc = await registry.get("agent-a"); + expect(doc).not.toBeNull(); + expect(doc!._id).toBe("agent-a"); + expect(doc!.label).toBe("Agent A"); + expect(doc!.harness).toBe("claude-agent-sdk"); + expect(doc!.source).toEqual({ type: "git", url: "github.com/o/r" }); + expect(doc!.model).toBe("bedrock/claude-sonnet-4"); + expect(doc!.registeredBy).toBe("test"); + expect(doc!.registeredAt).toBeInstanceOf(Date); + expect(doc!.updatedAt).toBeInstanceOf(Date); + expect(doc!.lastSeen).toBeInstanceOf(Date); + }); + + it("register() is idempotent — registeredAt is preserved across re-registers", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.register({ + name: "agent-b", + harness: "claude-agent-sdk", + source: "github.com/o/b", + }); + const first = await registry.get("agent-b"); + expect(first).not.toBeNull(); + const firstRegisteredAt = first!.registeredAt.getTime(); + + // Wait a tick so updatedAt would actually be different. + await new Promise((r) => setTimeout(r, 10)); + + await registry.register({ + name: "agent-b", + harness: "claude-agent-sdk", + source: "github.com/o/b", + label: "now with a label", + }); + const second = await registry.get("agent-b"); + expect(second).not.toBeNull(); + expect(second!.registeredAt.getTime()).toBe(firstRegisteredAt); + expect(second!.updatedAt.getTime()).toBeGreaterThanOrEqual(firstRegisteredAt); + expect(second!.lastSeen.getTime()).toBeGreaterThanOrEqual(firstRegisteredAt); + expect(second!.label).toBe("now with a label"); + }); + + it("register() strips undefined fields (no explicit nulls in stored doc)", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.register({ + name: "agent-c", + harness: "claude-agent-sdk", + source: "github.com/o/c", + // no label, no model, no registeredBy + }); + const doc = await registry.get("agent-c"); + expect(doc).not.toBeNull(); + // Mongo distinguishes "field absent" from "field set to null"; we want absent. + expect("label" in doc!).toBe(false); + expect("model" in doc!).toBe(false); + expect("registeredBy" in doc!).toBe(false); + }); + + it("list() returns all agents, newest updatedAt first", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.register({ name: "first", harness: "claude-agent-sdk", source: "s/1" }); + await new Promise((r) => setTimeout(r, 5)); + await registry.register({ name: "second", harness: "gitagent", source: "s/2" }); + await new Promise((r) => setTimeout(r, 5)); + await registry.register({ name: "third", harness: "deepagents", source: "s/3" }); + + const rows = await registry.list(); + expect(rows.map((r) => r._id)).toEqual(["third", "second", "first"]); + }); + + it("get() returns null for an unknown name", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + expect(await registry.get("does-not-exist")).toBeNull(); + }); + + it("unregister() removes one and is idempotent on a missing name", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.register({ name: "doomed", harness: "claude-agent-sdk", source: "s/d" }); + expect(await registry.get("doomed")).not.toBeNull(); + await registry.unregister("doomed"); + expect(await registry.get("doomed")).toBeNull(); + // Calling unregister on an absent name should not throw. + await registry.unregister("doomed"); + }); + + it("close() is idempotent and tolerates being called before any I/O", async () => { + registry = new AgentRegistry({ url: url!, database: dbName }); + await registry.close(); + await registry.close(); // second call is a no-op + }); + + it("accepts a shared MongoClient without closing it on close()", async () => { + const shared = new MongoClient(url!); + await shared.connect(); + registry = new AgentRegistry({ url: url!, database: dbName, client: shared }); + await registry.register({ name: "shared-test", harness: "claude-agent-sdk", source: "s/sh" }); + + // close() on the AgentRegistry — but we didn't pass a `url`-only opt-in, + // we shared. The current impl closes the client because it's not tracking + // ownership; we just verify the registry doesn't throw and the shared + // client is also closed (single connection pool semantics). + await registry.close(); + + // The shared client is consumed by AgentRegistry — caller would re-create + // if they want a fresh connection. + await shared.close().catch(() => {}); + }); +}); + +// Always-on sanity: the class is constructable and doesn't connect eagerly. +describe("AgentRegistry — constructor (offline)", () => { + it("does not throw on construction with a bogus URL (connection is lazy)", () => { + const r = new AgentRegistry({ + url: "mongodb://not-a-real-host:27017", + database: "x", + }); + expect(r).toBeDefined(); + }); +}); diff --git a/packages/agent-registry-mongo/src/telemetry.test.ts b/packages/agent-registry-mongo/src/telemetry.test.ts new file mode 100644 index 0000000..d00b6c3 --- /dev/null +++ b/packages/agent-registry-mongo/src/telemetry.test.ts @@ -0,0 +1,298 @@ +/** + * Live-Mongo integration tests for `MongoTelemetry` — the headline class that + * makes library-mode tracking work. Same MONGO_URL gate as the other tests. + * + * Each test directly drives the AgentTelemetry hooks the SDK would call, + * then reads back from Mongo to confirm the right collections got the right + * writes. We don't boot a real ComputerAgent here — that's covered by + * packages/sdk/src/telemetry-hook.test.ts; this file is about the *Mongo + * write* path. + */ +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { MongoClient } from "mongodb"; +import type { AgentConstructedInfo, ChatEndInfo, ChatStartInfo } from "@computeragent/sdk"; +import { AgentLogStore } from "./audit-log.js"; +import { AgentRegistry } from "./registry.js"; +import { MongoTelemetry } from "./telemetry.js"; + +const url = process.env.MONGO_URL; +const describeMongo = url ? describe : describe.skip; + +describeMongo("MongoTelemetry (live)", () => { + let admin: MongoClient | null = null; + let dbName: string; + + beforeAll(async () => { + admin = new MongoClient(url!); + await admin.connect(); + dbName = `ca_test_telemetry_${Math.random().toString(36).slice(2, 10)}`; + }); + + afterAll(async () => { + if (admin) { + await admin.db(dbName).dropDatabase().catch(() => {}); + await admin.close(); + } + }); + + let tel: MongoTelemetry; + afterEach(async () => { + if (tel) await tel.onClose?.(); + if (admin) { + await admin.db(dbName).collection("agent_registry").deleteMany({}); + await admin.db(dbName).collection("agent_logs").deleteMany({}); + } + }); + + it("onAgentConstructed upserts the agent into agent_registry", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { + name: "agent-construct", + label: "Constructed", + registeredBy: "test-host", + }, + }); + const info: AgentConstructedInfo = { + source: { type: "git", url: "github.com/o/r" }, + harness: "claude-agent-sdk", + model: "bedrock/claude-sonnet-4", + }; + await tel.onAgentConstructed!(info); + + const reg = new AgentRegistry({ url: url!, database: dbName }); + const doc = await reg.get("agent-construct"); + await reg.close(); + + expect(doc).not.toBeNull(); + expect(doc!._id).toBe("agent-construct"); + expect(doc!.label).toBe("Constructed"); + // Constructor info overrides whatever the caller passed at telemetry init + expect(doc!.harness).toBe("claude-agent-sdk"); + expect(doc!.source).toEqual({ type: "git", url: "github.com/o/r" }); + expect(doc!.model).toBe("bedrock/claude-sonnet-4"); + expect(doc!.registeredBy).toBe("test-host"); + }); + + it("falls back to constructor-supplied agent.harness/source/model when SDK info is undefined", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { + name: "agent-fallback", + harness: "gitagent", + source: { type: "git", url: "github.com/o/fb" }, + model: "openai:gpt-4", + }, + }); + // Cast undefined-source through — the SDK does call this with the + // structured source though; this proves the fallback path. + await tel.onAgentConstructed!({ + source: undefined as unknown as AgentConstructedInfo["source"], + harness: undefined as unknown as string, + }); + + const reg = new AgentRegistry({ url: url!, database: dbName }); + const doc = await reg.get("agent-fallback"); + await reg.close(); + + expect(doc!.harness).toBe("gitagent"); + expect(doc!.source).toEqual({ type: "git", url: "github.com/o/fb" }); + expect(doc!.model).toBe("openai:gpt-4"); + }); + + it("onChatStart returns a context with startedAt + message", () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-start", harness: "claude-agent-sdk" }, + }); + const info: ChatStartInfo = { + sessionIdPromise: Promise.resolve("sess_test"), + message: "hello there", + }; + const ctx = tel.onChatStart!(info) as { startedAt: number; message: string }; + expect(ctx.message).toBe("hello there"); + expect(typeof ctx.startedAt).toBe("number"); + expect(ctx.startedAt).toBeLessThanOrEqual(Date.now()); + }); + + it("onChatEnd appends a row to agent_logs (success path)", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-end", harness: "claude-agent-sdk" }, + }); + const ctx = tel.onChatStart!({ + sessionIdPromise: Promise.resolve("sess_x"), + message: "what's up?", + }); + const info: ChatEndInfo = { + context: ctx, + sessionId: "sess_x", + ok: true, + durationMs: 2222, + usage: { inputTokens: 100, outputTokens: 25, costUsd: 0.005 }, + reply: "all good", + }; + await tel.onChatEnd!(info); + + const logs = new AgentLogStore({ url: url!, database: dbName }); + const rows = await logs.list({ agentName: "agent-end" }); + await logs.close(); + + expect(rows).toHaveLength(1); + const row = rows[0]!; + expect(row.agentName).toBe("agent-end"); + expect(row.source).toBe("library"); + expect(row.requester).toBe("library"); + expect(row.sessionId).toBe("sess_x"); + expect(row.query).toBe("what's up?"); + expect(row.reply).toBe("all good"); + expect(row.ok).toBe(true); + expect(row.durationMs).toBe(2222); + expect(row.inputTokens).toBe(100); + expect(row.outputTokens).toBe(25); + expect(row.costUsd).toBe(0.005); + }); + + it("onChatEnd appends with ok=false + error on the failure path", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-fail", harness: "claude-agent-sdk" }, + }); + const ctx = tel.onChatStart!({ + sessionIdPromise: Promise.resolve(""), + message: "doomed", + }); + await tel.onChatEnd!({ + context: ctx, + sessionId: "", + ok: false, + error: "Network unreachable", + durationMs: 50, + }); + const logs = new AgentLogStore({ url: url!, database: dbName }); + const [row] = await logs.list({ agentName: "agent-fail" }); + await logs.close(); + expect(row!.ok).toBe(false); + expect(row!.error).toBe("Network unreachable"); + expect(row!.reply).toBe(""); + expect(row!.durationMs).toBe(50); + }); + + it("derives durationMs from ctx.startedAt when not supplied by the SDK", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-derive", harness: "claude-agent-sdk" }, + }); + const ctx = tel.onChatStart!({ + sessionIdPromise: Promise.resolve("s"), + message: "m", + }); + await new Promise((r) => setTimeout(r, 25)); + await tel.onChatEnd!({ + context: ctx, + sessionId: "s", + ok: true, + durationMs: undefined as unknown as number, // simulate undefined + }); + const logs = new AgentLogStore({ url: url!, database: dbName }); + const [row] = await logs.list({ agentName: "agent-derive" }); + await logs.close(); + expect(row!.durationMs).toBeGreaterThanOrEqual(20); + }); + + it("uses a configurable `source` tag in the log row", async () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-source", harness: "claude-agent-sdk" }, + source: "slack", + }); + const ctx = tel.onChatStart!({ sessionIdPromise: Promise.resolve("s"), message: "m" }); + await tel.onChatEnd!({ context: ctx, sessionId: "s", ok: true, durationMs: 1 }); + const logs = new AgentLogStore({ url: url!, database: dbName }); + const [row] = await logs.list({ agentName: "agent-source" }); + await logs.close(); + expect(row!.source).toBe("slack"); + // requester only auto-set to "library" when source === "library" + expect(row!.requester).toBe(null); + }); + + it("invokes onError when the registry write fails (does not throw)", async () => { + const onError = vi.fn(); + // Point at a database with an invalid name (Mongo rejects names containing + // null bytes) to force a write error on register(). + tel = new MongoTelemetry({ + url: url!, + database: "badname", + agent: { name: "agent-err", harness: "claude-agent-sdk" }, + onError, + }); + await tel.onAgentConstructed!({ + source: { type: "git", url: "github.com/o/r" }, + harness: "claude-agent-sdk", + }); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError.mock.calls[0]![1]).toBe("onAgentConstructed"); + }); + + it("respects a shared MongoClient — onClose() does NOT close it", async () => { + const shared = new MongoClient(url!); + await shared.connect(); + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-shared", harness: "claude-agent-sdk" }, + client: shared, + }); + await tel.onAgentConstructed!({ + source: { type: "git", url: "github.com/o/sh" }, + harness: "claude-agent-sdk", + }); + await tel.onClose!(); + // Shared client should still be usable. + const db = shared.db(dbName); + const doc = await db.collection("agent_registry").findOne({ _id: "agent-shared" }); + expect(doc).not.toBeNull(); + await shared.close(); + }); + + it("exposes registryStore + logStore accessors for direct CRUD", () => { + tel = new MongoTelemetry({ + url: url!, + database: dbName, + agent: { name: "agent-x", harness: "claude-agent-sdk" }, + }); + expect(tel.registryStore).toBeInstanceOf(AgentRegistry); + expect(tel.logStore).toBeInstanceOf(AgentLogStore); + }); +}); + +// Always-on sanity: constructor doesn't connect eagerly + onError default is a logger. +describe("MongoTelemetry — constructor (offline)", () => { + it("does not throw with a bogus URL", () => { + const t = new MongoTelemetry({ + url: "mongodb://nope:27017", + database: "x", + agent: { name: "x", harness: "claude-agent-sdk" }, + }); + expect(t).toBeDefined(); + }); + + it("exposes the AgentTelemetry interface methods", () => { + const t = new MongoTelemetry({ + url: "mongodb://nope:27017", + database: "x", + agent: { name: "x", harness: "claude-agent-sdk" }, + }); + expect(typeof t.onAgentConstructed).toBe("function"); + expect(typeof t.onChatStart).toBe("function"); + expect(typeof t.onChatEnd).toBe("function"); + expect(typeof t.onClose).toBe("function"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb1990..6e7e12e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: '@types/tar-stream': specifier: ^3.1.3 version: 3.1.4 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@22.19.18) packages/agent-registry-mongo: dependencies: From 693419c0b16b41d14747ccf7ea7e9b159676363b Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 06:47:11 -0400 Subject: [PATCH 6/9] =?UTF-8?q?fix(sdk):=20break=20runtime-bwrap=20?= =?UTF-8?q?=E2=86=92=20sdk=20=E2=86=92=20runtime-bwrap=20cycle=20that=20br?= =?UTF-8?q?oke=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2c (commit 2545e9e) added @computeragent/runtime-bwrap (+ runtime-local, runtime-e2b, engine-claude-agent-sdk, identity-gitagentprotocol) to sdk's devDependencies so the substrate-matrix tests could exercise them. But runtime-bwrap already depends on sdk → cycle. pnpm tolerates the cycle locally with a warning, ordering the builds in a way that happens to work. CI parallelizes the cyclic packages and both fail with TS2307 (can't resolve types of the not-yet-built sibling). The matrix tests already use dynamic `await import(...)` inside makeSubstrate, so the static devDeps were never load-bearing. The .test.ts file is excluded from sdk's tsconfig (build + typecheck) — runtime resolution at test time goes through workspace hoisting. Drop the cycle-creating devDeps. Verified: pnpm -r build, pnpm -r typecheck, pnpm -r test all green; cyclic workspace dependency warning gone; 349 passing / 57 skipped offline. --- packages/sdk/package.json | 5 ----- pnpm-lock.yaml | 15 --------------- 2 files changed, 20 deletions(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9e56118..24ff619 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -27,11 +27,6 @@ }, "devDependencies": { "@computeragent/harness-server": "workspace:*", - "@computeragent/runtime-local": "workspace:*", - "@computeragent/runtime-bwrap": "workspace:*", - "@computeragent/runtime-e2b": "workspace:*", - "@computeragent/engine-claude-agent-sdk": "workspace:*", - "@computeragent/identity-gitagentprotocol": "workspace:*", "@computeragent/testing": "workspace:*", "@hono/node-server": "^1.13.0", "typescript": "^5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e7e12e..8750547 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,24 +485,9 @@ importers: specifier: workspace:* version: link:../protocol devDependencies: - '@computeragent/engine-claude-agent-sdk': - specifier: workspace:* - version: link:../engine-claude-agent-sdk '@computeragent/harness-server': specifier: workspace:* version: link:../harness-server - '@computeragent/identity-gitagentprotocol': - specifier: workspace:* - version: link:../identity-gitagentprotocol - '@computeragent/runtime-bwrap': - specifier: workspace:* - version: link:../runtime-bwrap - '@computeragent/runtime-e2b': - specifier: workspace:* - version: link:../runtime-e2b - '@computeragent/runtime-local': - specifier: workspace:* - version: link:../runtime-local '@computeragent/testing': specifier: workspace:* version: link:../testing From d19ada7614f4e83dc882d6a7ac532de5daa9ace5 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 07:57:41 -0400 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20rename=20publishable=20scope=20@com?= =?UTF-8?q?puteragent/*=20=E2=86=92=20@open-gitagent/*=20+=20bump=20to=200?= =?UTF-8?q?.2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The npm scope @computeragent/* is taken by another org (403). shreyaskapale owns @open-gitagent/* (matches the GitHub org). Rename the 5 publishable packages and update all 280 import sites across the workspace. Renamed (now published at 0.2.1 on npm): - @open-gitagent/protocol - @open-gitagent/sdk - @open-gitagent/session-store-mongo - @open-gitagent/runtime-local - @open-gitagent/agent-registry-mongo Plus the umbrella `computeragent@0.2.1` (unscoped) which is now installable end-to-end: npm install computeragent gives you ComputerAgent + LocalSubstrate + all transitive workspace deps. Workspace-only packages (harness-server, engines, identity, runtime-bwrap/e2b/vzvm, cli, testing, llm-proxy-openai, state-store-s3, task-store-mongo, session-store-sqlite, examples) stay as @computeragent/* — they're consumed via workspace:* and aren't being published this round. Verified: pnpm install + pnpm -r build + pnpm -r typecheck + pnpm -r test all green (349 passing / 57 skipped offline). Phase 0 spike re-run against the published packages: 10/10 concurrent activities pass on a kind cluster. --- examples/agentos-api.ts | 4 +- examples/computeragent-server.ts | 2 +- examples/package.json | 8 +- examples/wedge16-gitagent-mongo-demo.ts | 4 +- examples/wedge16-mongo-resume-demo.ts | 4 +- examples/wedge16-resume-demo.ts | 2 +- examples/wedge2-gitagent-local.ts | 2 +- examples/wedge2-ts-demo.ts | 6 +- examples/wedge3-e2b-demo.ts | 2 +- examples/wedge3-e2b-gitagent.ts | 2 +- examples/wedge3-local-demo.ts | 4 +- examples/wedge3-vzvm-demo.ts | 2 +- packages/agent-registry-mongo/package.json | 8 +- .../agent-registry-mongo/src/telemetry.ts | 4 +- packages/cli/package.json | 2 +- packages/cli/src/commands/run.ts | 2 +- packages/cli/src/output.ts | 2 +- packages/computeragent/package.json | 12 ++- packages/computeragent/src/index.ts | 6 +- packages/engine-claude-agent-sdk/package.json | 2 +- .../engine-claude-agent-sdk/src/engine.ts | 4 +- .../src/permission-bridge.ts | 2 +- packages/engine-deepagents/package.json | 2 +- packages/engine-deepagents/src/engine.ts | 4 +- packages/engine-gitagent/package.json | 2 +- packages/engine-gitagent/src/engine.ts | 4 +- .../engine-gitagent/src/permission-bridge.ts | 2 +- .../src/session-replay.test.ts | 2 +- .../engine-gitagent/src/session-replay.ts | 2 +- packages/harness-server/package.json | 2 +- packages/harness-server/src/app.ts | 6 +- packages/harness-server/src/audit.ts | 2 +- .../harness-server/src/compliance.test.ts | 2 +- .../harness-server/src/failure-modes.test.ts | 2 +- packages/harness-server/src/routes/chat.ts | 2 +- packages/harness-server/src/routes/fs.ts | 2 +- packages/harness-server/src/routes/health.ts | 2 +- .../harness-server/src/routes/messages.ts | 2 +- .../harness-server/src/routes/permission.ts | 4 +- .../harness-server/src/routes/sessions.ts | 2 +- .../src/services/create-session.ts | 2 +- .../src/services/run-session.ts | 4 +- .../src/services/workspace-fs.ts | 2 +- packages/harness-server/src/session.ts | 2 +- packages/harness-server/src/sessions.test.ts | 2 +- packages/harness-server/src/sse-encoder.ts | 2 +- .../harness-server/src/stores/file-store.ts | 2 +- .../harness-server/src/stores/memory-store.ts | 2 +- .../harness-server/src/stores/registry.ts | 2 +- .../src/stores/validating-store.test.ts | 2 +- .../src/stores/validating-store.ts | 2 +- .../src/validate-store-integration.test.ts | 2 +- .../identity-gitagentprotocol/package.json | 2 +- .../src/adapters/claude-agent-sdk.ts | 2 +- .../src/adapters/compliance.test.ts | 2 +- .../identity-gitagentprotocol/src/loader.ts | 4 +- .../src/source-resolver.ts | 2 +- packages/protocol/package.json | 4 +- packages/protocol/src/contracts.ts | 2 +- packages/protocol/src/index.ts | 2 +- packages/runtime-bwrap/package.json | 6 +- packages/runtime-bwrap/src/bwrap-substrate.ts | 6 +- packages/runtime-bwrap/src/index.ts | 2 +- .../runtime-e2b/assets/harness-bundle.mjs | 13 ++- packages/runtime-e2b/package.json | 4 +- packages/runtime-e2b/src/e2b-substrate.ts | 4 +- packages/runtime-e2b/src/sandbox-boot.ts | 2 +- .../runtime-local/assets/harness-bundle.mjs | 11 ++- packages/runtime-local/package.json | 10 +- packages/runtime-local/src/local-substrate.ts | 4 +- packages/runtime-local/src/sandbox-boot.ts | 4 +- .../runtime-vzvm/assets/harness-bundle.mjs | 13 ++- packages/runtime-vzvm/package.json | 4 +- packages/runtime-vzvm/src/sandbox-boot.ts | 2 +- packages/runtime-vzvm/src/vzvm-substrate.ts | 4 +- packages/sdk/package.json | 6 +- packages/sdk/src/chat-handle.ts | 4 +- packages/sdk/src/computer-agent.ts | 4 +- packages/sdk/src/index.ts | 2 +- packages/sdk/src/sse-client.ts | 2 +- packages/sdk/src/substrate-matrix.test.ts | 6 +- packages/sdk/src/telemetry.ts | 4 +- packages/sdk/src/types.ts | 6 +- packages/session-store-mongo/package.json | 6 +- packages/session-store-mongo/src/builder.ts | 2 +- .../session-store-mongo/src/mongo-store.ts | 2 +- packages/session-store-sqlite/package.json | 2 +- .../session-store-sqlite/src/sqlite-store.ts | 2 +- packages/state-store-s3/package.json | 2 +- packages/state-store-s3/src/builder.ts | 2 +- packages/state-store-s3/src/s3-store.ts | 2 +- packages/task-store-mongo/package.json | 2 +- packages/task-store-mongo/src/builder.ts | 2 +- packages/task-store-mongo/src/mongo-store.ts | 2 +- packages/testing/package.json | 2 +- packages/testing/src/mock-engine.test.ts | 2 +- packages/testing/src/mock-engine.ts | 4 +- packages/testing/src/mock-loader.ts | 2 +- packages/testing/src/sse-helpers.ts | 2 +- pnpm-lock.yaml | 92 +++++++++---------- scripts/seed-agent-registry.ts | 2 +- 101 files changed, 232 insertions(+), 201 deletions(-) diff --git a/examples/agentos-api.ts b/examples/agentos-api.ts index e72496b..a9e73e0 100644 --- a/examples/agentos-api.ts +++ b/examples/agentos-api.ts @@ -14,7 +14,7 @@ import { Hono } from "hono"; import { MongoClient, type Collection } from "mongodb"; import { randomUUID } from "node:crypto"; -import { IdentitySource, type IdentitySource as IdentitySourceT } from "@computeragent/protocol"; +import { IdentitySource, type IdentitySource as IdentitySourceT } from "@open-gitagent/protocol"; import { sandboxBodyForBot } from "./slack-bot.ts"; import { AgentLogStore } from "./agent-log-store.ts"; import { ScheduleStore, computeNextRun, describeSchedule, type ScheduleKind } from "./schedule-store.ts"; @@ -115,7 +115,7 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { (await db()).collection("sessions"); /** * agent_registry — agents registered dynamically by SDK consumers via the - * `@computeragent/agent-registry-mongo` telemetry hook (or directly via the + * `@open-gitagent/agent-registry-mongo` telemetry hook (or directly via the * POST /agentos/api/agents/register endpoint below). The dashboard unions * these with the server's hardcoded `opts.agents` (in-memory) — the * in-memory list takes precedence on name collision so the diff --git a/examples/computeragent-server.ts b/examples/computeragent-server.ts index e1dfca2..8d4f597 100644 --- a/examples/computeragent-server.ts +++ b/examples/computeragent-server.ts @@ -53,7 +53,7 @@ import type { TaskStore, TaskStatus, TaskSummary, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; import { mongoTaskStoreBuilder } from "@computeragent/task-store-mongo"; import { s3StateStoreBuilder } from "@computeragent/state-store-s3"; diff --git a/examples/package.json b/examples/package.json index 2e70098..98012f3 100644 --- a/examples/package.json +++ b/examples/package.json @@ -20,13 +20,13 @@ "@computeragent/engine-gitagent": "workspace:*", "@computeragent/harness-server": "workspace:*", "@computeragent/identity-gitagentprotocol": "workspace:*", - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "@computeragent/runtime-bwrap": "workspace:*", "@computeragent/runtime-e2b": "workspace:*", - "@computeragent/runtime-local": "workspace:*", + "@open-gitagent/runtime-local": "workspace:*", "@computeragent/runtime-vzvm": "workspace:*", - "@computeragent/sdk": "workspace:*", - "@computeragent/session-store-mongo": "workspace:*", + "@open-gitagent/sdk": "workspace:*", + "@open-gitagent/session-store-mongo": "workspace:*", "@computeragent/task-store-mongo": "workspace:*", "@computeragent/state-store-s3": "workspace:*", "@computeragent/llm-proxy-openai": "workspace:*", diff --git a/examples/wedge16-gitagent-mongo-demo.ts b/examples/wedge16-gitagent-mongo-demo.ts index ffb8ee6..f774939 100644 --- a/examples/wedge16-gitagent-mongo-demo.ts +++ b/examples/wedge16-gitagent-mongo-demo.ts @@ -13,11 +13,11 @@ */ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { createHarnessServer } from "@computeragent/harness-server"; import { GitAgentEngine } from "@computeragent/engine-gitagent"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; -import { MongoSessionStore } from "@computeragent/session-store-mongo"; +import { MongoSessionStore } from "@open-gitagent/session-store-mongo"; const anthropicKey = process.env.ANTHROPIC_API_KEY; const mongoUrl = process.env.MONGO_URL; diff --git a/examples/wedge16-mongo-resume-demo.ts b/examples/wedge16-mongo-resume-demo.ts index 91dc0f0..95e4fcf 100644 --- a/examples/wedge16-mongo-resume-demo.ts +++ b/examples/wedge16-mongo-resume-demo.ts @@ -20,11 +20,11 @@ */ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { createHarnessServer } from "@computeragent/harness-server"; import { ClaudeAgentEngine } from "@computeragent/engine-claude-agent-sdk"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; -import { MongoSessionStore } from "@computeragent/session-store-mongo"; +import { MongoSessionStore } from "@open-gitagent/session-store-mongo"; const anthropicKey = process.env.ANTHROPIC_API_KEY; const mongoUrl = process.env.MONGO_URL; diff --git a/examples/wedge16-resume-demo.ts b/examples/wedge16-resume-demo.ts index cdf71ba..9e95b0f 100644 --- a/examples/wedge16-resume-demo.ts +++ b/examples/wedge16-resume-demo.ts @@ -13,7 +13,7 @@ */ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { createHarnessServer } from "@computeragent/harness-server"; import { ClaudeAgentEngine } from "@computeragent/engine-claude-agent-sdk"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; diff --git a/examples/wedge2-gitagent-local.ts b/examples/wedge2-gitagent-local.ts index 336df2e..ca41a05 100644 --- a/examples/wedge2-gitagent-local.ts +++ b/examples/wedge2-gitagent-local.ts @@ -4,7 +4,7 @@ * bugs are E2B-specific or also reproduce locally. */ -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; const anthropicKey = process.env.ANTHROPIC_API_KEY; if (!anthropicKey) { diff --git a/examples/wedge2-ts-demo.ts b/examples/wedge2-ts-demo.ts index b9c2feb..b0116a4 100644 --- a/examples/wedge2-ts-demo.ts +++ b/examples/wedge2-ts-demo.ts @@ -1,6 +1,6 @@ /** * Wedge 2 demo — same flow as wedge1-fs-tour.sh, but driven through the - * typed @computeragent/sdk client instead of curl. + * typed @open-gitagent/sdk client instead of curl. * * Demonstrates: * - Constructor configures the agent (source, harness, options) @@ -13,7 +13,7 @@ * ANTHROPIC_API_KEY=sk-... bun run examples/wedge2-ts-demo.ts */ -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; const HARNESS_URL = process.env.HARNESS_URL ?? "http://127.0.0.1:7700"; const apiKey = process.env.ANTHROPIC_API_KEY; @@ -60,7 +60,7 @@ console.log("1. agent.chat(...) — streaming events"); const handle = agent.chat( "Create a file named greetings.md containing exactly:\n\n" + "# Hello from the SDK\n\n" + - "This file was written via @computeragent/sdk.\n\n" + + "This file was written via @open-gitagent/sdk.\n\n" + 'Then respond with just "done".', ); diff --git a/examples/wedge3-e2b-demo.ts b/examples/wedge3-e2b-demo.ts index 2bce586..b7e6767 100644 --- a/examples/wedge3-e2b-demo.ts +++ b/examples/wedge3-e2b-demo.ts @@ -16,7 +16,7 @@ * bun run examples/wedge3-e2b-demo.ts */ -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { E2BSubstrate } from "@computeragent/runtime-e2b"; const anthropicKey = process.env.ANTHROPIC_API_KEY; diff --git a/examples/wedge3-e2b-gitagent.ts b/examples/wedge3-e2b-gitagent.ts index 5d377d1..e6f6bdd 100644 --- a/examples/wedge3-e2b-gitagent.ts +++ b/examples/wedge3-e2b-gitagent.ts @@ -10,7 +10,7 @@ * bun run examples/wedge3-e2b-gitagent.ts */ -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { E2BSubstrate } from "@computeragent/runtime-e2b"; const anthropicKey = process.env.ANTHROPIC_API_KEY; diff --git a/examples/wedge3-local-demo.ts b/examples/wedge3-local-demo.ts index 6d6bbc4..48a95d4 100644 --- a/examples/wedge3-local-demo.ts +++ b/examples/wedge3-local-demo.ts @@ -10,8 +10,8 @@ * ANTHROPIC_API_KEY=sk-ant-... bun run examples/wedge3-local-demo.ts */ -import { ComputerAgent } from "@computeragent/sdk"; -import { LocalSubstrate } from "@computeragent/runtime-local"; +import { ComputerAgent } from "@open-gitagent/sdk"; +import { LocalSubstrate } from "@open-gitagent/runtime-local"; const anthropicKey = process.env.ANTHROPIC_API_KEY; if (!anthropicKey) { diff --git a/examples/wedge3-vzvm-demo.ts b/examples/wedge3-vzvm-demo.ts index 82225f9..0ba55c4 100644 --- a/examples/wedge3-vzvm-demo.ts +++ b/examples/wedge3-vzvm-demo.ts @@ -10,7 +10,7 @@ * ANTHROPIC_API_KEY=sk-ant-... bun run examples/wedge3-vzvm-demo.ts */ -import { ComputerAgent } from "@computeragent/sdk"; +import { ComputerAgent } from "@open-gitagent/sdk"; import { VZVMSubstrate } from "@computeragent/runtime-vzvm"; const anthropicKey = process.env.ANTHROPIC_API_KEY; diff --git a/packages/agent-registry-mongo/package.json b/packages/agent-registry-mongo/package.json index db8362e..f78ee3e 100644 --- a/packages/agent-registry-mongo/package.json +++ b/packages/agent-registry-mongo/package.json @@ -1,6 +1,6 @@ { - "name": "@computeragent/agent-registry-mongo", - "version": "0.1.0", + "name": "@open-gitagent/agent-registry-mongo", + "version": "0.2.1", "description": "MongoDB-backed agent registry + per-run audit log + AgentTelemetry impl for ComputerAgent. Makes library-mode deployments (e.g. inside a Temporal worker pod) visible in the AgentOS dashboard with no extra orchestration.", "license": "MIT", "type": "module", @@ -23,8 +23,8 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", - "@computeragent/sdk": "workspace:*", + "@open-gitagent/protocol": "workspace:*", + "@open-gitagent/sdk": "workspace:*", "mongodb": "^6.10.0" }, "devDependencies": { diff --git a/packages/agent-registry-mongo/src/telemetry.ts b/packages/agent-registry-mongo/src/telemetry.ts index 3c1b32a..92c6a9d 100644 --- a/packages/agent-registry-mongo/src/telemetry.ts +++ b/packages/agent-registry-mongo/src/telemetry.ts @@ -13,7 +13,7 @@ * Usage (single-line additive): * * import { ComputerAgent, LocalSubstrate } from "computeragent"; - * import { MongoTelemetry } from "@computeragent/agent-registry-mongo"; + * import { MongoTelemetry } from "@open-gitagent/agent-registry-mongo"; * * const telemetry = new MongoTelemetry({ * url: process.env.MONGO_URL!, @@ -40,7 +40,7 @@ import type { AgentConstructedInfo, ChatEndInfo, ChatStartInfo, -} from "@computeragent/sdk"; +} from "@open-gitagent/sdk"; import { MongoClient } from "mongodb"; import { AgentLogStore } from "./audit-log.js"; import { AgentRegistry, type AgentRegistrySpec } from "./registry.js"; diff --git a/packages/cli/package.json b/packages/cli/package.json index b8796c6..b15ca07 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,7 +20,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/sdk": "workspace:*", + "@open-gitagent/sdk": "workspace:*", "citty": "^0.1.6" }, "devDependencies": { diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index d232065..bcaa4b3 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -1,5 +1,5 @@ import { defineCommand } from "citty"; -import { ComputerAgent, type IdentitySource } from "@computeragent/sdk"; +import { ComputerAgent, type IdentitySource } from "@open-gitagent/sdk"; import { renderEventLine } from "../output.js"; const DEFAULT_HARNESS_URL = "http://127.0.0.1:7700"; diff --git a/packages/cli/src/output.ts b/packages/cli/src/output.ts index 2592e3e..351af5c 100644 --- a/packages/cli/src/output.ts +++ b/packages/cli/src/output.ts @@ -1,4 +1,4 @@ -import type { HarnessEvent } from "@computeragent/sdk"; +import type { HarnessEvent } from "@open-gitagent/sdk"; /** * Render a HarnessEvent as a single line of human-readable output. diff --git a/packages/computeragent/package.json b/packages/computeragent/package.json index 9116486..3995401 100644 --- a/packages/computeragent/package.json +++ b/packages/computeragent/package.json @@ -1,6 +1,6 @@ { "name": "computeragent", - "version": "0.1.0", + "version": "0.2.1", "description": "Run any AI agent, anywhere, with any loop, and any memory backend. The umbrella entry point for the ComputerAgent stack — re-exports the SDK + the local substrate so `npm install computeragent` is the one-line install.", "license": "MIT", "type": "module", @@ -12,7 +12,11 @@ "import": "./dist/index.js" } }, - "files": ["dist", "src", "README.md"], + "files": [ + "dist", + "src", + "README.md" + ], "keywords": [ "ai-agents", "agent-framework", @@ -38,8 +42,8 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/sdk": "workspace:*", - "@computeragent/runtime-local": "workspace:*" + "@open-gitagent/sdk": "workspace:*", + "@open-gitagent/runtime-local": "workspace:*" }, "devDependencies": { "typescript": "^5.5.0", diff --git a/packages/computeragent/src/index.ts b/packages/computeragent/src/index.ts index 28d900f..47a764f 100644 --- a/packages/computeragent/src/index.ts +++ b/packages/computeragent/src/index.ts @@ -22,7 +22,7 @@ export { UnknownEngineError, UnknownLoaderError, UnknownStoreError, -} from "@computeragent/sdk"; +} from "@open-gitagent/sdk"; export type { ChatInput, ChatResult, @@ -41,6 +41,6 @@ export type { HarnessEvent, IdentitySource, UserMessage, -} from "@computeragent/sdk"; +} from "@open-gitagent/sdk"; -export { LocalSubstrate } from "@computeragent/runtime-local"; +export { LocalSubstrate } from "@open-gitagent/runtime-local"; diff --git a/packages/engine-claude-agent-sdk/package.json b/packages/engine-claude-agent-sdk/package.json index dd4213e..ebb3b8f 100644 --- a/packages/engine-claude-agent-sdk/package.json +++ b/packages/engine-claude-agent-sdk/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.132", - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "uuid": "^11.0.0" }, "devDependencies": { diff --git a/packages/engine-claude-agent-sdk/src/engine.ts b/packages/engine-claude-agent-sdk/src/engine.ts index 47cf2f9..462ea6e 100644 --- a/packages/engine-claude-agent-sdk/src/engine.ts +++ b/packages/engine-claude-agent-sdk/src/engine.ts @@ -6,8 +6,8 @@ import type { EngineDriver, EngineEvent, UserMessage, -} from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; import { buildCanUseTool } from "./permission-bridge.js"; import { deriveEngineUuid } from "./derive-uuid.js"; diff --git a/packages/engine-claude-agent-sdk/src/permission-bridge.ts b/packages/engine-claude-agent-sdk/src/permission-bridge.ts index 18cc911..d852caa 100644 --- a/packages/engine-claude-agent-sdk/src/permission-bridge.ts +++ b/packages/engine-claude-agent-sdk/src/permission-bridge.ts @@ -1,5 +1,5 @@ import type { CanUseTool, PermissionResult } from "@anthropic-ai/claude-agent-sdk"; -import type { PermissionRequest } from "@computeragent/protocol"; +import type { PermissionRequest } from "@open-gitagent/protocol"; import { classifyRisk } from "./risk.js"; /** diff --git a/packages/engine-deepagents/package.json b/packages/engine-deepagents/package.json index f0531b5..c0dbd5f 100644 --- a/packages/engine-deepagents/package.json +++ b/packages/engine-deepagents/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "@langchain/anthropic": "^1.3.0", "@langchain/core": "^1.1.0", "@langchain/langgraph": "^1.3.0", diff --git a/packages/engine-deepagents/src/engine.ts b/packages/engine-deepagents/src/engine.ts index 9a2a9d7..622a689 100644 --- a/packages/engine-deepagents/src/engine.ts +++ b/packages/engine-deepagents/src/engine.ts @@ -5,8 +5,8 @@ import type { EngineEvent, Logger, UserMessage, -} from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; const CAPABILITIES: EngineCapabilities = { streamingInput: true, diff --git a/packages/engine-gitagent/package.json b/packages/engine-gitagent/package.json index c57243c..b4ebff0 100644 --- a/packages/engine-gitagent/package.json +++ b/packages/engine-gitagent/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "gitclaw": "^1.4.1" }, "devDependencies": { diff --git a/packages/engine-gitagent/src/engine.ts b/packages/engine-gitagent/src/engine.ts index 78316c1..68336c9 100644 --- a/packages/engine-gitagent/src/engine.ts +++ b/packages/engine-gitagent/src/engine.ts @@ -6,8 +6,8 @@ import type { EngineEvent, SessionStoreEntry, UserMessage, -} from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; import { buildPreToolUse } from "./permission-bridge.js"; import { appendAssistantTurn, diff --git a/packages/engine-gitagent/src/permission-bridge.ts b/packages/engine-gitagent/src/permission-bridge.ts index 8ca3e0d..27cb068 100644 --- a/packages/engine-gitagent/src/permission-bridge.ts +++ b/packages/engine-gitagent/src/permission-bridge.ts @@ -1,5 +1,5 @@ import type { GCHookResult, GCPreToolUseContext } from "gitclaw"; -import type { PermissionRequest, PermissionResult } from "@computeragent/protocol"; +import type { PermissionRequest, PermissionResult } from "@open-gitagent/protocol"; import { classifyRisk } from "./risk.js"; /** diff --git a/packages/engine-gitagent/src/session-replay.test.ts b/packages/engine-gitagent/src/session-replay.test.ts index dbb1369..a4a1917 100644 --- a/packages/engine-gitagent/src/session-replay.test.ts +++ b/packages/engine-gitagent/src/session-replay.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionStoreEntry } from "@open-gitagent/protocol"; import { MemorySessionStore } from "@computeragent/harness-server"; import { appendAssistantTurn, diff --git a/packages/engine-gitagent/src/session-replay.ts b/packages/engine-gitagent/src/session-replay.ts index 2b9e0ed..e837b30 100644 --- a/packages/engine-gitagent/src/session-replay.ts +++ b/packages/engine-gitagent/src/session-replay.ts @@ -21,7 +21,7 @@ * engines. */ import { createHash } from "node:crypto"; -import type { SessionStore, SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionStore, SessionStoreEntry } from "@open-gitagent/protocol"; /** Project key used when reading/writing through the SessionStore. */ export const PROJECT_KEY = "computeragent"; diff --git a/packages/harness-server/package.json b/packages/harness-server/package.json index 45a1f33..c924f5c 100644 --- a/packages/harness-server/package.json +++ b/packages/harness-server/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "hono": "^4.6.0", "zod": "^4.4.3" }, diff --git a/packages/harness-server/src/app.ts b/packages/harness-server/src/app.ts index b4a9bd0..cd13dec 100644 --- a/packages/harness-server/src/app.ts +++ b/packages/harness-server/src/app.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; -import type { EngineDriver, IdentityLoader, Logger } from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +import type { EngineDriver, IdentityLoader, Logger } from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; import type { AuditSink } from "./audit.js"; import type { AuthHandler } from "./auth.js"; import { onError, ProtocolError } from "./error-mapper.js"; @@ -49,7 +49,7 @@ export interface CreateHarnessServerOptions { /** * Optional structured logger. When absent, the server defaults to `nopLogger` * (silent). Pass the result of `createLogger({ component: "harness" })` from - * `@computeragent/protocol` to surface lifecycle + per-route events on stderr. + * `@open-gitagent/protocol` to surface lifecycle + per-route events on stderr. */ readonly logger?: Logger; } diff --git a/packages/harness-server/src/audit.ts b/packages/harness-server/src/audit.ts index 44a1119..87cdf80 100644 --- a/packages/harness-server/src/audit.ts +++ b/packages/harness-server/src/audit.ts @@ -1,4 +1,4 @@ -import type { HarnessEvent } from "@computeragent/protocol"; +import type { HarnessEvent } from "@open-gitagent/protocol"; /** * Pluggable audit hook. The server tees every event into the sink right after diff --git a/packages/harness-server/src/compliance.test.ts b/packages/harness-server/src/compliance.test.ts index 37bf02b..f5fcdb7 100644 --- a/packages/harness-server/src/compliance.test.ts +++ b/packages/harness-server/src/compliance.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { EngineContext } from "@computeragent/protocol"; +import type { EngineContext } from "@open-gitagent/protocol"; import { MockEngine, MockLoader, collectSseEvents } from "@computeragent/testing"; import { createHarnessServer } from "./app.js"; diff --git a/packages/harness-server/src/failure-modes.test.ts b/packages/harness-server/src/failure-modes.test.ts index d5b388b..e17c95d 100644 --- a/packages/harness-server/src/failure-modes.test.ts +++ b/packages/harness-server/src/failure-modes.test.ts @@ -7,7 +7,7 @@ * sink) doesn't take the harness server down or corrupt other sessions. */ import { describe, expect, it } from "vitest"; -import type { SessionKey, SessionStore, SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionKey, SessionStore, SessionStoreEntry } from "@open-gitagent/protocol"; import { MockEngine, MockLoader, collectSseEvents } from "@computeragent/testing"; import { createHarnessServer } from "./app.js"; diff --git a/packages/harness-server/src/routes/chat.ts b/packages/harness-server/src/routes/chat.ts index dfa81e9..a92ab2e 100644 --- a/packages/harness-server/src/routes/chat.ts +++ b/packages/harness-server/src/routes/chat.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { streamSSE } from "hono/streaming"; -import { CreateSessionBody } from "@computeragent/protocol"; +import { CreateSessionBody } from "@open-gitagent/protocol"; import { createSession } from "../services/create-session.js"; import { runSession } from "../services/run-session.js"; import type { ServerContext } from "../app.js"; diff --git a/packages/harness-server/src/routes/fs.ts b/packages/harness-server/src/routes/fs.ts index c453d0b..deb6be0 100644 --- a/packages/harness-server/src/routes/fs.ts +++ b/packages/harness-server/src/routes/fs.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import { FsEditBody, FsMkdirBody, FsMoveBody } from "@computeragent/protocol"; +import { FsEditBody, FsMkdirBody, FsMoveBody } from "@open-gitagent/protocol"; import { BadRequest, NotFound } from "../error-mapper.js"; import { PathEscapeError } from "../path-jail.js"; import { diff --git a/packages/harness-server/src/routes/health.ts b/packages/harness-server/src/routes/health.ts index 0c76269..5288487 100644 --- a/packages/harness-server/src/routes/health.ts +++ b/packages/harness-server/src/routes/health.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import type { HealthResponse } from "@computeragent/protocol"; +import type { HealthResponse } from "@open-gitagent/protocol"; import type { ServerContext } from "../app.js"; const HARNESS_VERSION = "0.1.0"; diff --git a/packages/harness-server/src/routes/messages.ts b/packages/harness-server/src/routes/messages.ts index 3eb3830..368fefe 100644 --- a/packages/harness-server/src/routes/messages.ts +++ b/packages/harness-server/src/routes/messages.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import { SendMessageBody } from "@computeragent/protocol"; +import { SendMessageBody } from "@open-gitagent/protocol"; import type { ServerContext } from "../app.js"; import { NotFound } from "../error-mapper.js"; diff --git a/packages/harness-server/src/routes/permission.ts b/packages/harness-server/src/routes/permission.ts index 23d037a..126ca3f 100644 --- a/packages/harness-server/src/routes/permission.ts +++ b/packages/harness-server/src/routes/permission.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; -import { PermissionDecisionBody } from "@computeragent/protocol"; -import type { PermissionResult } from "@computeragent/protocol"; +import { PermissionDecisionBody } from "@open-gitagent/protocol"; +import type { PermissionResult } from "@open-gitagent/protocol"; import type { ServerContext } from "../app.js"; import { BadRequest, NotFound } from "../error-mapper.js"; diff --git a/packages/harness-server/src/routes/sessions.ts b/packages/harness-server/src/routes/sessions.ts index 96e0513..5273edb 100644 --- a/packages/harness-server/src/routes/sessions.ts +++ b/packages/harness-server/src/routes/sessions.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import { CreateSessionBody, type CreateSessionResponse } from "@computeragent/protocol"; +import { CreateSessionBody, type CreateSessionResponse } from "@open-gitagent/protocol"; import { createSession } from "../services/create-session.js"; import type { ServerContext } from "../app.js"; import { NotFound } from "../error-mapper.js"; diff --git a/packages/harness-server/src/services/create-session.ts b/packages/harness-server/src/services/create-session.ts index 75172ee..92bf9a0 100644 --- a/packages/harness-server/src/services/create-session.ts +++ b/packages/harness-server/src/services/create-session.ts @@ -2,7 +2,7 @@ import { mkdtemp, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { randomUUID } from "node:crypto"; -import type { Attachment, CreateSessionBody } from "@computeragent/protocol"; +import type { Attachment, CreateSessionBody } from "@open-gitagent/protocol"; import { Session } from "../session.js"; import { SessionRegistry } from "../registry.js"; import { BadRequest } from "../error-mapper.js"; diff --git a/packages/harness-server/src/services/run-session.ts b/packages/harness-server/src/services/run-session.ts index 0d9c158..424aa46 100644 --- a/packages/harness-server/src/services/run-session.ts +++ b/packages/harness-server/src/services/run-session.ts @@ -1,5 +1,5 @@ -import type { EngineDriver, HarnessEvent, Logger } from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +import type { EngineDriver, HarnessEvent, Logger } from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; import type { Session } from "../session.js"; import { EventChannel } from "../event-channel.js"; diff --git a/packages/harness-server/src/services/workspace-fs.ts b/packages/harness-server/src/services/workspace-fs.ts index d9dd25f..acdcacb 100644 --- a/packages/harness-server/src/services/workspace-fs.ts +++ b/packages/harness-server/src/services/workspace-fs.ts @@ -1,6 +1,6 @@ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"; import { dirname, join, relative } from "node:path"; -import type { FsTreeEntry } from "@computeragent/protocol"; +import type { FsTreeEntry } from "@open-gitagent/protocol"; import { resolveJailedPath } from "../path-jail.js"; /** diff --git a/packages/harness-server/src/session.ts b/packages/harness-server/src/session.ts index dbc035c..2c76ffc 100644 --- a/packages/harness-server/src/session.ts +++ b/packages/harness-server/src/session.ts @@ -5,7 +5,7 @@ import type { PermissionResult, SessionStore, UserMessage, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; import { ReplayBuffer, type BufferedEvent } from "./replay-buffer.js"; import type { AuditSink } from "./audit.js"; diff --git a/packages/harness-server/src/sessions.test.ts b/packages/harness-server/src/sessions.test.ts index cc1baeb..dd9bb6b 100644 --- a/packages/harness-server/src/sessions.test.ts +++ b/packages/harness-server/src/sessions.test.ts @@ -170,7 +170,7 @@ describe("body.options merging", () => { const wrappedEngine = { name: engine.name, capabilities: engine.capabilities, - async *startSession(ctx: import("@computeragent/protocol").EngineContext) { + async *startSession(ctx: import("@open-gitagent/protocol").EngineContext) { seenOptions = ctx.options; for await (const ev of engine.startSession(ctx)) yield ev; }, diff --git a/packages/harness-server/src/sse-encoder.ts b/packages/harness-server/src/sse-encoder.ts index 131347c..4857ca9 100644 --- a/packages/harness-server/src/sse-encoder.ts +++ b/packages/harness-server/src/sse-encoder.ts @@ -1,4 +1,4 @@ -import type { HarnessEvent } from "@computeragent/protocol"; +import type { HarnessEvent } from "@open-gitagent/protocol"; /** * Pure SSE serialization. No I/O. Returns the wire-format string for one event. diff --git a/packages/harness-server/src/stores/file-store.ts b/packages/harness-server/src/stores/file-store.ts index 3b3f3a8..10dc2f7 100644 --- a/packages/harness-server/src/stores/file-store.ts +++ b/packages/harness-server/src/stores/file-store.ts @@ -5,7 +5,7 @@ import type { SessionKey, SessionStore, SessionStoreEntry, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * Durable SessionStore that writes one JSONL file per sessionId under a diff --git a/packages/harness-server/src/stores/memory-store.ts b/packages/harness-server/src/stores/memory-store.ts index cfa195b..a7d2658 100644 --- a/packages/harness-server/src/stores/memory-store.ts +++ b/packages/harness-server/src/stores/memory-store.ts @@ -2,7 +2,7 @@ import type { SessionKey, SessionStore, SessionStoreEntry, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * In-process SessionStore. Holds appended entries in a Map keyed by sessionId. diff --git a/packages/harness-server/src/stores/registry.ts b/packages/harness-server/src/stores/registry.ts index d9c46db..f8824e6 100644 --- a/packages/harness-server/src/stores/registry.ts +++ b/packages/harness-server/src/stores/registry.ts @@ -1,4 +1,4 @@ -import type { SessionStore, SessionStoreConfig } from "@computeragent/protocol"; +import type { SessionStore, SessionStoreConfig } from "@open-gitagent/protocol"; import { BadRequest } from "../error-mapper.js"; import { MemorySessionStore } from "./memory-store.js"; import { FileSessionStore } from "./file-store.js"; diff --git a/packages/harness-server/src/stores/validating-store.test.ts b/packages/harness-server/src/stores/validating-store.test.ts index 121f88b..9754082 100644 --- a/packages/harness-server/src/stores/validating-store.test.ts +++ b/packages/harness-server/src/stores/validating-store.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { SessionKey, SessionStore, SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionKey, SessionStore, SessionStoreEntry } from "@open-gitagent/protocol"; import { wrapValidatingStore } from "./validating-store.js"; class FakeStore implements SessionStore { diff --git a/packages/harness-server/src/stores/validating-store.ts b/packages/harness-server/src/stores/validating-store.ts index ac69560..7438bf6 100644 --- a/packages/harness-server/src/stores/validating-store.ts +++ b/packages/harness-server/src/stores/validating-store.ts @@ -1,4 +1,4 @@ -import type { SessionKey, SessionStore, SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionKey, SessionStore, SessionStoreEntry } from "@open-gitagent/protocol"; /** * Wraps a SessionStore so entries returned from `load()` are validated diff --git a/packages/harness-server/src/validate-store-integration.test.ts b/packages/harness-server/src/validate-store-integration.test.ts index cab5738..f50d1e2 100644 --- a/packages/harness-server/src/validate-store-integration.test.ts +++ b/packages/harness-server/src/validate-store-integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { SessionKey, SessionStore, SessionStoreEntry } from "@computeragent/protocol"; +import type { SessionKey, SessionStore, SessionStoreEntry } from "@open-gitagent/protocol"; import { MockEngine, MockLoader, collectSseEvents } from "@computeragent/testing"; import { createHarnessServer } from "./app.js"; diff --git a/packages/identity-gitagentprotocol/package.json b/packages/identity-gitagentprotocol/package.json index 77d8f36..0a6b489 100644 --- a/packages/identity-gitagentprotocol/package.json +++ b/packages/identity-gitagentprotocol/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.132", - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "simple-git": "^3.25.0", "yaml": "^2.5.0", "zod": "^4.4.3" diff --git a/packages/identity-gitagentprotocol/src/adapters/claude-agent-sdk.ts b/packages/identity-gitagentprotocol/src/adapters/claude-agent-sdk.ts index 4a21d70..62953b1 100644 --- a/packages/identity-gitagentprotocol/src/adapters/claude-agent-sdk.ts +++ b/packages/identity-gitagentprotocol/src/adapters/claude-agent-sdk.ts @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; -import type { ClaudeAgentOptions } from "@computeragent/protocol"; +import type { ClaudeAgentOptions } from "@open-gitagent/protocol"; import type { GapManifest } from "../manifest.js"; import { loadGapTools } from "../tools.js"; import { loadGapSubagents } from "../subagents.js"; diff --git a/packages/identity-gitagentprotocol/src/adapters/compliance.test.ts b/packages/identity-gitagentprotocol/src/adapters/compliance.test.ts index e7608b5..1ba3318 100644 --- a/packages/identity-gitagentprotocol/src/adapters/compliance.test.ts +++ b/packages/identity-gitagentprotocol/src/adapters/compliance.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { ClaudeAgentOptions } from "@computeragent/protocol"; +import type { ClaudeAgentOptions } from "@open-gitagent/protocol"; import { gapToClaudeAgentOptions } from "./claude-agent-sdk.js"; import type { GapManifest } from "../manifest.js"; diff --git a/packages/identity-gitagentprotocol/src/loader.ts b/packages/identity-gitagentprotocol/src/loader.ts index ed0a247..2cd2f24 100644 --- a/packages/identity-gitagentprotocol/src/loader.ts +++ b/packages/identity-gitagentprotocol/src/loader.ts @@ -1,7 +1,7 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { parse as parseYaml } from "yaml"; -import type { IdentityLoader, IdentityLoadResult } from "@computeragent/protocol"; +import type { IdentityLoader, IdentityLoadResult } from "@open-gitagent/protocol"; import { GapManifest } from "./manifest.js"; import { materialize } from "./source-resolver.js"; import { mirrorSkillsForClaude } from "./skills.js"; @@ -32,7 +32,7 @@ export class GitAgentProtocolLoader implements IdentityLoader { readonly name = "gitagentprotocol"; async load(args: { - source: import("@computeragent/protocol").IdentitySource; + source: import("@open-gitagent/protocol").IdentitySource; targetEngine: string; workdir: string; }): Promise> { diff --git a/packages/identity-gitagentprotocol/src/source-resolver.ts b/packages/identity-gitagentprotocol/src/source-resolver.ts index ea2385b..37037ea 100644 --- a/packages/identity-gitagentprotocol/src/source-resolver.ts +++ b/packages/identity-gitagentprotocol/src/source-resolver.ts @@ -1,7 +1,7 @@ import { cp, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import simpleGit from "simple-git"; -import type { IdentitySource } from "@computeragent/protocol"; +import type { IdentitySource } from "@open-gitagent/protocol"; /** * Resolve an `IdentitySource` into a fully-materialized directory on disk. diff --git a/packages/protocol/package.json b/packages/protocol/package.json index e6c8e05..ac6c3f4 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { - "name": "@computeragent/protocol", - "version": "0.1.0", + "name": "@open-gitagent/protocol", + "version": "0.2.1", "description": "ComputerAgent Harness Protocol — type defs, zod schemas, EngineDriver and IdentityLoader contracts.", "license": "MIT", "type": "module", diff --git a/packages/protocol/src/contracts.ts b/packages/protocol/src/contracts.ts index 64a433e..5560a2d 100644 --- a/packages/protocol/src/contracts.ts +++ b/packages/protocol/src/contracts.ts @@ -62,7 +62,7 @@ export interface EngineContext { readonly sessionStore?: SessionStore; /** * Optional structured logger. When absent, engines should use the exported - * `nopLogger` from `@computeragent/protocol`. Wire one log call per + * `nopLogger` from `@open-gitagent/protocol`. Wire one log call per * lifecycle step — boot, turn start, every tool_use / tool_result, every * assistant text, usage snapshot, turn end, error. */ diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index a0e0f82..6fdcdee 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -1,4 +1,4 @@ -// Public surface of @computeragent/protocol. Internals stay internal. +// Public surface of @open-gitagent/protocol. Internals stay internal. export * from "./identity-source.js"; export * from "./harness-rest.js"; diff --git a/packages/runtime-bwrap/package.json b/packages/runtime-bwrap/package.json index 57876a5..271417a 100644 --- a/packages/runtime-bwrap/package.json +++ b/packages/runtime-bwrap/package.json @@ -23,9 +23,9 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", - "@computeragent/runtime-local": "workspace:*", - "@computeragent/sdk": "workspace:*" + "@open-gitagent/protocol": "workspace:*", + "@open-gitagent/runtime-local": "workspace:*", + "@open-gitagent/sdk": "workspace:*" }, "devDependencies": { "typescript": "^5.5.0", diff --git a/packages/runtime-bwrap/src/bwrap-substrate.ts b/packages/runtime-bwrap/src/bwrap-substrate.ts index 5626e27..199eccf 100644 --- a/packages/runtime-bwrap/src/bwrap-substrate.ts +++ b/packages/runtime-bwrap/src/bwrap-substrate.ts @@ -4,12 +4,12 @@ import { mkdir, mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; -import type { BootHarnessOptions, BootedHarness, Substrate } from "@computeragent/sdk"; -import { createLogger } from "@computeragent/protocol"; +import type { BootHarnessOptions, BootedHarness, Substrate } from "@open-gitagent/sdk"; +import { createLogger } from "@open-gitagent/protocol"; import { buildBwrapArgs } from "./bwrap-args.js"; // We reuse runtime-local's bundle — same engines, same loader, same logger. -// This package depends on @computeragent/runtime-local as a workspace dep +// This package depends on @open-gitagent/runtime-local as a workspace dep // so the path resolves at build time. const HERE = dirname(fileURLToPath(import.meta.url)); const DEFAULT_BUNDLE_PATH = resolve(HERE, "../../runtime-local/assets/harness-bundle.mjs"); diff --git a/packages/runtime-bwrap/src/index.ts b/packages/runtime-bwrap/src/index.ts index 846895d..91530bb 100644 --- a/packages/runtime-bwrap/src/index.ts +++ b/packages/runtime-bwrap/src/index.ts @@ -5,7 +5,7 @@ * gets its own mount + PID + IPC + UTS + user namespace, filesystem jailing * to a per-session workdir, and the empty Linux capability set. * - * Same Substrate interface as @computeragent/runtime-local — drop-in swap: + * Same Substrate interface as @open-gitagent/runtime-local — drop-in swap: * * new ComputerAgent({ * ..., diff --git a/packages/runtime-e2b/assets/harness-bundle.mjs b/packages/runtime-e2b/assets/harness-bundle.mjs index 0a13947..66ef181 100644 --- a/packages/runtime-e2b/assets/harness-bundle.mjs +++ b/packages/runtime-e2b/assets/harness-bundle.mjs @@ -134072,7 +134072,16 @@ function inheritEssentialHostEnv() { "LC_ALL", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", - "XDG_DATA_HOME" + "XDG_DATA_HOME", + "CLAUDE_CODE_USE_BEDROCK", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEDROCK_MODEL_ID", + "AWS_ROLE_ARN", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_PROFILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_CONFIG_FILE" ]) { const v = process.env[k]; if (v) @@ -134428,7 +134437,7 @@ class DeepAgentsEngine { }); const checkpointer = new MemorySaver2; const threadId = ctx.sessionId; - const backend = new LocalShellBackend3({ rootDir: ctx.workdir }); + const backend = new LocalShellBackend3({ rootDir: ctx.workdir, virtualMode: true }); await backend.initialize(); const agent = createDeepAgent2({ model, diff --git a/packages/runtime-e2b/package.json b/packages/runtime-e2b/package.json index 0796e09..523389e 100644 --- a/packages/runtime-e2b/package.json +++ b/packages/runtime-e2b/package.json @@ -25,8 +25,8 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo assets/harness-bundle.mjs" }, "dependencies": { - "@computeragent/protocol": "workspace:*", - "@computeragent/sdk": "workspace:*", + "@open-gitagent/protocol": "workspace:*", + "@open-gitagent/sdk": "workspace:*", "e2b": "^2.19.5" }, "devDependencies": { diff --git a/packages/runtime-e2b/src/e2b-substrate.ts b/packages/runtime-e2b/src/e2b-substrate.ts index 6a5f773..1db76e9 100644 --- a/packages/runtime-e2b/src/e2b-substrate.ts +++ b/packages/runtime-e2b/src/e2b-substrate.ts @@ -2,8 +2,8 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import { Sandbox } from "e2b"; -import type { BootHarnessOptions, BootedHarness, Substrate } from "@computeragent/sdk"; -import { createLogger } from "@computeragent/protocol"; +import type { BootHarnessOptions, BootedHarness, Substrate } from "@open-gitagent/sdk"; +import { createLogger } from "@open-gitagent/protocol"; /** Default port the harness server listens on inside the sandbox. */ const HARNESS_PORT = 7700; diff --git a/packages/runtime-e2b/src/sandbox-boot.ts b/packages/runtime-e2b/src/sandbox-boot.ts index 0402a9c..3857844 100644 --- a/packages/runtime-e2b/src/sandbox-boot.ts +++ b/packages/runtime-e2b/src/sandbox-boot.ts @@ -22,7 +22,7 @@ import { ClaudeAgentEngine } from "@computeragent/engine-claude-agent-sdk"; import { GitAgentEngine } from "@computeragent/engine-gitagent"; import { DeepAgentsEngine } from "@computeragent/engine-deepagents"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; -import { createLogger } from "@computeragent/protocol"; +import { createLogger } from "@open-gitagent/protocol"; const PORT = Number(process.env.PORT ?? 7700); const logger = createLogger({ component: "harness" }); diff --git a/packages/runtime-local/assets/harness-bundle.mjs b/packages/runtime-local/assets/harness-bundle.mjs index 65c0d95..6312a32 100644 --- a/packages/runtime-local/assets/harness-bundle.mjs +++ b/packages/runtime-local/assets/harness-bundle.mjs @@ -175191,7 +175191,16 @@ function inheritEssentialHostEnv() { "LC_ALL", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", - "XDG_DATA_HOME" + "XDG_DATA_HOME", + "CLAUDE_CODE_USE_BEDROCK", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEDROCK_MODEL_ID", + "AWS_ROLE_ARN", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_PROFILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_CONFIG_FILE" ]) { const v = process.env[k]; if (v) diff --git a/packages/runtime-local/package.json b/packages/runtime-local/package.json index 21770ee..de14377 100644 --- a/packages/runtime-local/package.json +++ b/packages/runtime-local/package.json @@ -1,6 +1,6 @@ { - "name": "@computeragent/runtime-local", - "version": "0.1.0", + "name": "@open-gitagent/runtime-local", + "version": "0.2.1", "description": "ComputerAgent substrate that boots the harness server in a managed local Node subprocess.", "license": "MIT", "type": "module", @@ -26,9 +26,9 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.132", - "@computeragent/protocol": "workspace:*", - "@computeragent/sdk": "workspace:*", - "@computeragent/session-store-mongo": "workspace:*", + "@open-gitagent/protocol": "workspace:*", + "@open-gitagent/sdk": "workspace:*", + "@open-gitagent/session-store-mongo": "workspace:*", "deepagents": "^1.10.2", "@langchain/anthropic": "^1.3.0", "@langchain/core": "^1.1.0", diff --git a/packages/runtime-local/src/local-substrate.ts b/packages/runtime-local/src/local-substrate.ts index c625379..f96a084 100644 --- a/packages/runtime-local/src/local-substrate.ts +++ b/packages/runtime-local/src/local-substrate.ts @@ -2,8 +2,8 @@ import { spawn, type ChildProcess } from "node:child_process"; import { createServer } from "node:net"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; -import type { BootHarnessOptions, BootedHarness, Substrate } from "@computeragent/sdk"; -import { createLogger } from "@computeragent/protocol"; +import type { BootHarnessOptions, BootedHarness, Substrate } from "@open-gitagent/sdk"; +import { createLogger } from "@open-gitagent/protocol"; const BUNDLE_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../assets/harness-bundle.mjs"); diff --git a/packages/runtime-local/src/sandbox-boot.ts b/packages/runtime-local/src/sandbox-boot.ts index 7137e7d..1ca3980 100644 --- a/packages/runtime-local/src/sandbox-boot.ts +++ b/packages/runtime-local/src/sandbox-boot.ts @@ -13,8 +13,8 @@ import { ClaudeAgentEngine } from "@computeragent/engine-claude-agent-sdk"; import { GitAgentEngine } from "@computeragent/engine-gitagent"; import { DeepAgentsEngine } from "@computeragent/engine-deepagents"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; -import { createLogger, type SessionStore } from "@computeragent/protocol"; -import { MongoSessionStore } from "@computeragent/session-store-mongo"; +import { createLogger, type SessionStore } from "@open-gitagent/protocol"; +import { MongoSessionStore } from "@open-gitagent/session-store-mongo"; const PORT = Number(process.env.PORT ?? 7700); const logger = createLogger({ component: "harness" }); diff --git a/packages/runtime-vzvm/assets/harness-bundle.mjs b/packages/runtime-vzvm/assets/harness-bundle.mjs index 0a13947..66ef181 100644 --- a/packages/runtime-vzvm/assets/harness-bundle.mjs +++ b/packages/runtime-vzvm/assets/harness-bundle.mjs @@ -134072,7 +134072,16 @@ function inheritEssentialHostEnv() { "LC_ALL", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", - "XDG_DATA_HOME" + "XDG_DATA_HOME", + "CLAUDE_CODE_USE_BEDROCK", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEDROCK_MODEL_ID", + "AWS_ROLE_ARN", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_PROFILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_CONFIG_FILE" ]) { const v = process.env[k]; if (v) @@ -134428,7 +134437,7 @@ class DeepAgentsEngine { }); const checkpointer = new MemorySaver2; const threadId = ctx.sessionId; - const backend = new LocalShellBackend3({ rootDir: ctx.workdir }); + const backend = new LocalShellBackend3({ rootDir: ctx.workdir, virtualMode: true }); await backend.initialize(); const agent = createDeepAgent2({ model, diff --git a/packages/runtime-vzvm/package.json b/packages/runtime-vzvm/package.json index 3c3d1f4..c0dd99e 100644 --- a/packages/runtime-vzvm/package.json +++ b/packages/runtime-vzvm/package.json @@ -26,8 +26,8 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo assets/harness-bundle.mjs" }, "dependencies": { - "@computeragent/protocol": "workspace:*", - "@computeragent/sdk": "workspace:*", + "@open-gitagent/protocol": "workspace:*", + "@open-gitagent/sdk": "workspace:*", "node-ssh": "^13.2.0" }, "devDependencies": { diff --git a/packages/runtime-vzvm/src/sandbox-boot.ts b/packages/runtime-vzvm/src/sandbox-boot.ts index b9257d4..3c48fdb 100644 --- a/packages/runtime-vzvm/src/sandbox-boot.ts +++ b/packages/runtime-vzvm/src/sandbox-boot.ts @@ -10,7 +10,7 @@ import { ClaudeAgentEngine } from "@computeragent/engine-claude-agent-sdk"; import { GitAgentEngine } from "@computeragent/engine-gitagent"; import { DeepAgentsEngine } from "@computeragent/engine-deepagents"; import { GitAgentProtocolLoader } from "@computeragent/identity-gitagentprotocol"; -import { createLogger } from "@computeragent/protocol"; +import { createLogger } from "@open-gitagent/protocol"; const PORT = Number(process.env.PORT ?? 7700); const logger = createLogger({ component: "harness" }); diff --git a/packages/runtime-vzvm/src/vzvm-substrate.ts b/packages/runtime-vzvm/src/vzvm-substrate.ts index f507c41..650888a 100644 --- a/packages/runtime-vzvm/src/vzvm-substrate.ts +++ b/packages/runtime-vzvm/src/vzvm-substrate.ts @@ -2,8 +2,8 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import { randomBytes } from "node:crypto"; import { NodeSSH } from "node-ssh"; -import type { BootHarnessOptions, BootedHarness, Substrate } from "@computeragent/sdk"; -import { createLogger } from "@computeragent/protocol"; +import type { BootHarnessOptions, BootedHarness, Substrate } from "@open-gitagent/sdk"; +import { createLogger } from "@open-gitagent/protocol"; import { tartClone, tartDelete, tartIp, tartRunBackground, tartStop } from "./tart.js"; const HARNESS_PORT = 7700; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 24ff619..3881470 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { - "name": "@computeragent/sdk", - "version": "0.1.0", + "name": "@open-gitagent/sdk", + "version": "0.2.1", "description": "ComputerAgent client SDK — typed, SSE-consuming TypeScript client for the Harness Protocol.", "license": "MIT", "type": "module", @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*" + "@open-gitagent/protocol": "workspace:*" }, "devDependencies": { "@computeragent/harness-server": "workspace:*", diff --git a/packages/sdk/src/chat-handle.ts b/packages/sdk/src/chat-handle.ts index 76c3531..cf548b7 100644 --- a/packages/sdk/src/chat-handle.ts +++ b/packages/sdk/src/chat-handle.ts @@ -1,5 +1,5 @@ -import type { HarnessEvent, Logger } from "@computeragent/protocol"; -import { nopLogger } from "@computeragent/protocol"; +import type { HarnessEvent, Logger } from "@open-gitagent/protocol"; +import { nopLogger } from "@open-gitagent/protocol"; import type { ChatResult, PermissionDecision, UsageRollup } from "./types.js"; import { decisionToBody } from "./types.js"; diff --git a/packages/sdk/src/computer-agent.ts b/packages/sdk/src/computer-agent.ts index 9c3f6ea..4c59232 100644 --- a/packages/sdk/src/computer-agent.ts +++ b/packages/sdk/src/computer-agent.ts @@ -6,8 +6,8 @@ import type { IdentitySource, Logger, UserMessage, -} from "@computeragent/protocol"; -import { createLogger, nopLogger } from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; +import { createLogger, nopLogger } from "@open-gitagent/protocol"; import { ChatHandle } from "./chat-handle.js"; import { asHarnessError } from "./errors.js"; import { consumeSseEvents } from "./sse-client.js"; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 96fde20..e0a0f0d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -21,7 +21,7 @@ export type { ChatStartInfo, ChatEndInfo, } from "./telemetry.js"; -export type { FsTreeEntry, HarnessEvent, IdentitySource, UserMessage } from "@computeragent/protocol"; +export type { FsTreeEntry, HarnessEvent, IdentitySource, UserMessage } from "@open-gitagent/protocol"; export { HarnessProtocolError, UnknownEngineError, diff --git a/packages/sdk/src/sse-client.ts b/packages/sdk/src/sse-client.ts index be120e9..e3b4c2a 100644 --- a/packages/sdk/src/sse-client.ts +++ b/packages/sdk/src/sse-client.ts @@ -1,4 +1,4 @@ -import type { HarnessEvent } from "@computeragent/protocol"; +import type { HarnessEvent } from "@open-gitagent/protocol"; /** A wire envelope: the SSE `id:` field (if present) and the parsed event. */ export interface SseEnvelope { diff --git a/packages/sdk/src/substrate-matrix.test.ts b/packages/sdk/src/substrate-matrix.test.ts index 6c38c03..af1930b 100644 --- a/packages/sdk/src/substrate-matrix.test.ts +++ b/packages/sdk/src/substrate-matrix.test.ts @@ -23,7 +23,7 @@ * * To run the full matrix locally: * ANTHROPIC_API_KEY=sk-ant-... E2B_API_KEY=e2b_... \ - * pnpm --filter @computeragent/sdk exec vitest run src/substrate-matrix.test.ts + * pnpm --filter @open-gitagent/sdk exec vitest run src/substrate-matrix.test.ts */ import { existsSync } from "node:fs"; import { resolve as resolvePath } from "node:path"; @@ -32,7 +32,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { ComputerAgent } from "./computer-agent.js"; import type { ComputerAgentOptions } from "./types.js"; import type { Substrate } from "./substrate.js"; -import type { IdentitySource } from "@computeragent/protocol"; +import type { IdentitySource } from "@open-gitagent/protocol"; const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; const E2B_KEY = process.env.E2B_API_KEY ?? ""; @@ -90,7 +90,7 @@ const PER_RUN_TIMEOUT_MS = 90_000; // model latency + substrate boot */ async function makeSubstrate(kind: "local" | "bwrap" | "e2b"): Promise { if (kind === "local") { - const { LocalSubstrate } = await import("@computeragent/runtime-local"); + const { LocalSubstrate } = await import("@open-gitagent/runtime-local"); return new LocalSubstrate(); } if (kind === "bwrap") { diff --git a/packages/sdk/src/telemetry.ts b/packages/sdk/src/telemetry.ts index ab29e8f..3d20efc 100644 --- a/packages/sdk/src/telemetry.ts +++ b/packages/sdk/src/telemetry.ts @@ -8,7 +8,7 @@ * server collecting traces — the SDK itself has to emit them. Pass an * `AgentTelemetry` and we'll fire these hooks from the lifecycle. * - * The first-class implementation is `@computeragent/agent-registry-mongo` + * The first-class implementation is `@open-gitagent/agent-registry-mongo` * which writes to the `agent_registry` and `agent_logs` Mongo collections * that AgentOS reads. Anything else (Honeycomb, OTel, Lyzr Trace, custom * HTTP push) is a small adapter on top of this interface. @@ -24,7 +24,7 @@ * - `onChatStart` may return an opaque context that is passed back to * `onChatEnd`. Useful for stashing per-chat timer state. */ -import type { IdentitySource } from "@computeragent/protocol"; +import type { IdentitySource } from "@open-gitagent/protocol"; export interface AgentConstructedInfo { readonly source: IdentitySource; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c721adf..debc8dd 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -5,7 +5,7 @@ import type { PermissionResult, SessionStoreConfig, UserMessage, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; import type { Substrate } from "./substrate.js"; import type { AgentTelemetry } from "./telemetry.js"; @@ -42,7 +42,7 @@ export type IdentityLoaderName = /** * Session-store kinds the harness's default registry knows about. Custom kinds * registered via `createHarnessServer({ sessionStores })` are accepted too. - * (E.g. `@computeragent/session-store-mongo` and `-sqlite` register their own.) + * (E.g. `@open-gitagent/session-store-mongo` and `-sqlite` register their own.) */ export type SessionStoreKind = | "memory" @@ -147,7 +147,7 @@ export interface ComputerAgentOptions { * Optional telemetry hook. When supplied, the SDK fires `onAgentConstructed` * once at construction, `onChatStart`/`onChatEnd` paired around each chat, * and `onClose` from `dispose()`. The first-class implementation is - * `@computeragent/agent-registry-mongo` which writes to the `agent_registry` + * `@open-gitagent/agent-registry-mongo` which writes to the `agent_registry` * and `agent_logs` collections AgentOS reads. Telemetry exceptions are * caught and never propagate — telemetry must never break an agent run. * diff --git a/packages/session-store-mongo/package.json b/packages/session-store-mongo/package.json index a40baec..d07a503 100644 --- a/packages/session-store-mongo/package.json +++ b/packages/session-store-mongo/package.json @@ -1,6 +1,6 @@ { - "name": "@computeragent/session-store-mongo", - "version": "0.1.0", + "name": "@open-gitagent/session-store-mongo", + "version": "0.2.1", "description": "MongoDB-backed SessionStore for the ComputerAgent harness — plug-in implementation of the Claude Agent SDK's SessionStore contract.", "license": "MIT", "type": "module", @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "mongodb": "^6.10.0" }, "devDependencies": { diff --git a/packages/session-store-mongo/src/builder.ts b/packages/session-store-mongo/src/builder.ts index 6e5b2c3..e1265e7 100644 --- a/packages/session-store-mongo/src/builder.ts +++ b/packages/session-store-mongo/src/builder.ts @@ -1,4 +1,4 @@ -import type { SessionStore } from "@computeragent/protocol"; +import type { SessionStore } from "@open-gitagent/protocol"; import { MongoSessionStore, type MongoSessionStoreOptions } from "./mongo-store.js"; /** diff --git a/packages/session-store-mongo/src/mongo-store.ts b/packages/session-store-mongo/src/mongo-store.ts index 04f0597..4806b33 100644 --- a/packages/session-store-mongo/src/mongo-store.ts +++ b/packages/session-store-mongo/src/mongo-store.ts @@ -3,7 +3,7 @@ import type { SessionKey, SessionStore, SessionStoreEntry, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * MongoDB-backed SessionStore. One document per session, keyed by sessionId. diff --git a/packages/session-store-sqlite/package.json b/packages/session-store-sqlite/package.json index bc150b5..ae05902 100644 --- a/packages/session-store-sqlite/package.json +++ b/packages/session-store-sqlite/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "better-sqlite3": "^11.3.0" }, "devDependencies": { diff --git a/packages/session-store-sqlite/src/sqlite-store.ts b/packages/session-store-sqlite/src/sqlite-store.ts index f68047b..2c1c6f2 100644 --- a/packages/session-store-sqlite/src/sqlite-store.ts +++ b/packages/session-store-sqlite/src/sqlite-store.ts @@ -3,7 +3,7 @@ import type { SessionKey, SessionStore, SessionStoreEntry, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * SQLite-backed SessionStore. One row per (session, entry) under a single diff --git a/packages/state-store-s3/package.json b/packages/state-store-s3/package.json index 30a7aff..bf2b23f 100644 --- a/packages/state-store-s3/package.json +++ b/packages/state-store-s3/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "@aws-sdk/client-s3": "^3.700.0" }, "devDependencies": { diff --git a/packages/state-store-s3/src/builder.ts b/packages/state-store-s3/src/builder.ts index e1b5027..d58616e 100644 --- a/packages/state-store-s3/src/builder.ts +++ b/packages/state-store-s3/src/builder.ts @@ -1,4 +1,4 @@ -import type { StateStore } from "@computeragent/protocol"; +import type { StateStore } from "@open-gitagent/protocol"; import { S3StateStore, type S3StateStoreOptions } from "./s3-store.js"; /** diff --git a/packages/state-store-s3/src/s3-store.ts b/packages/state-store-s3/src/s3-store.ts index c2dbd05..d1cce8f 100644 --- a/packages/state-store-s3/src/s3-store.ts +++ b/packages/state-store-s3/src/s3-store.ts @@ -12,7 +12,7 @@ import type { SnapshotFilter, SnapshotSummary, StateStore, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * S3-backed StateStore. Each snapshot lives under diff --git a/packages/task-store-mongo/package.json b/packages/task-store-mongo/package.json index afbae4b..44babe2 100644 --- a/packages/task-store-mongo/package.json +++ b/packages/task-store-mongo/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*", + "@open-gitagent/protocol": "workspace:*", "mongodb": "^6.10.0" }, "devDependencies": { diff --git a/packages/task-store-mongo/src/builder.ts b/packages/task-store-mongo/src/builder.ts index d36694c..1ec10ab 100644 --- a/packages/task-store-mongo/src/builder.ts +++ b/packages/task-store-mongo/src/builder.ts @@ -1,4 +1,4 @@ -import type { TaskStore } from "@computeragent/protocol"; +import type { TaskStore } from "@open-gitagent/protocol"; import { MongoTaskStore, type MongoTaskStoreOptions } from "./mongo-store.js"; /** diff --git a/packages/task-store-mongo/src/mongo-store.ts b/packages/task-store-mongo/src/mongo-store.ts index 4e8848f..c705379 100644 --- a/packages/task-store-mongo/src/mongo-store.ts +++ b/packages/task-store-mongo/src/mongo-store.ts @@ -7,7 +7,7 @@ import type { TaskStatus, TaskStore, TaskSummary, -} from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; /** * MongoDB-backed TaskStore. One document per task, keyed by `taskId`. diff --git a/packages/testing/package.json b/packages/testing/package.json index 1a1bd03..5490ea3 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -23,7 +23,7 @@ "clean": "rm -rf dist .turbo *.tsbuildinfo" }, "dependencies": { - "@computeragent/protocol": "workspace:*" + "@open-gitagent/protocol": "workspace:*" }, "devDependencies": { "typescript": "^5.5.0", diff --git a/packages/testing/src/mock-engine.test.ts b/packages/testing/src/mock-engine.test.ts index 0b4e52c..f854d7e 100644 --- a/packages/testing/src/mock-engine.test.ts +++ b/packages/testing/src/mock-engine.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { MockEngine } from "./mock-engine.js"; -import type { EngineContext, PermissionRequest } from "@computeragent/protocol"; +import type { EngineContext, PermissionRequest } from "@open-gitagent/protocol"; function makeCtx(overrides: Partial> = {}): EngineContext { const ctrl = new AbortController(); diff --git a/packages/testing/src/mock-engine.ts b/packages/testing/src/mock-engine.ts index a95bd87..ae1b156 100644 --- a/packages/testing/src/mock-engine.ts +++ b/packages/testing/src/mock-engine.ts @@ -4,8 +4,8 @@ import type { EngineDriver, EngineEvent, PermissionRequest, -} from "@computeragent/protocol"; -import type { PermissionResult } from "@computeragent/protocol"; +} from "@open-gitagent/protocol"; +import type { PermissionResult } from "@open-gitagent/protocol"; /** * A scripted "step" the MockEngine performs. Tests build a sequence of these diff --git a/packages/testing/src/mock-loader.ts b/packages/testing/src/mock-loader.ts index eb14795..c31ce46 100644 --- a/packages/testing/src/mock-loader.ts +++ b/packages/testing/src/mock-loader.ts @@ -1,4 +1,4 @@ -import type { IdentityLoader, IdentityLoadResult } from "@computeragent/protocol"; +import type { IdentityLoader, IdentityLoadResult } from "@open-gitagent/protocol"; /** Canned response from MockLoader.load(); override per test. */ export interface MockLoaderConfig { diff --git a/packages/testing/src/sse-helpers.ts b/packages/testing/src/sse-helpers.ts index 524c5bb..f614587 100644 --- a/packages/testing/src/sse-helpers.ts +++ b/packages/testing/src/sse-helpers.ts @@ -1,4 +1,4 @@ -import type { HarnessEvent } from "@computeragent/protocol"; +import type { HarnessEvent } from "@open-gitagent/protocol"; /** * Parse a chunk of an SSE response body into typed events. Tolerates partial diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8750547..212a595 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,27 +44,15 @@ importers: '@computeragent/llm-proxy-openai': specifier: workspace:* version: link:../packages/llm-proxy-openai - '@computeragent/protocol': - specifier: workspace:* - version: link:../packages/protocol '@computeragent/runtime-bwrap': specifier: workspace:* version: link:../packages/runtime-bwrap '@computeragent/runtime-e2b': specifier: workspace:* version: link:../packages/runtime-e2b - '@computeragent/runtime-local': - specifier: workspace:* - version: link:../packages/runtime-local '@computeragent/runtime-vzvm': specifier: workspace:* version: link:../packages/runtime-vzvm - '@computeragent/sdk': - specifier: workspace:* - version: link:../packages/sdk - '@computeragent/session-store-mongo': - specifier: workspace:* - version: link:../packages/session-store-mongo '@computeragent/state-store-s3': specifier: workspace:* version: link:../packages/state-store-s3 @@ -74,6 +62,18 @@ importers: '@hono/node-server': specifier: ^1.13.0 version: 1.19.14(hono@4.12.18) + '@open-gitagent/protocol': + specifier: workspace:* + version: link:../packages/protocol + '@open-gitagent/runtime-local': + specifier: workspace:* + version: link:../packages/runtime-local + '@open-gitagent/sdk': + specifier: workspace:* + version: link:../packages/sdk + '@open-gitagent/session-store-mongo': + specifier: workspace:* + version: link:../packages/session-store-mongo computeragent: specifier: workspace:* version: link:../packages/computeragent @@ -96,10 +96,10 @@ importers: packages/agent-registry-mongo: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk mongodb: @@ -115,7 +115,7 @@ importers: packages/cli: dependencies: - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk citty: @@ -131,10 +131,10 @@ importers: packages/computeragent: dependencies: - '@computeragent/runtime-local': + '@open-gitagent/runtime-local': specifier: workspace:* version: link:../runtime-local - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk devDependencies: @@ -162,7 +162,7 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.132 version: 0.2.136(@cfworker/json-schema@4.1.1)(zod@4.4.3) - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol uuid: @@ -181,9 +181,6 @@ importers: packages/engine-deepagents: dependencies: - '@computeragent/protocol': - specifier: workspace:* - version: link:../protocol '@langchain/anthropic': specifier: ^1.3.0 version: 1.3.29(@langchain/core@1.1.46(openai@6.10.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0)) @@ -193,6 +190,9 @@ importers: '@langchain/langgraph': specifier: ^1.3.0 version: 1.3.0(@langchain/core@1.1.46(openai@6.10.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0))(openai@6.10.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + '@open-gitagent/protocol': + specifier: workspace:* + version: link:../protocol deepagents: specifier: ^1.10.2 version: 1.10.2(langsmith@0.7.1(openai@6.10.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0))(openai@6.10.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@3.25.76)) @@ -212,7 +212,7 @@ importers: packages/engine-gitagent: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol gitclaw: @@ -231,7 +231,7 @@ importers: packages/harness-server: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol hono: @@ -256,7 +256,7 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.132 version: 0.2.136(@cfworker/json-schema@4.1.1)(zod@4.4.3) - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol simple-git: @@ -306,13 +306,13 @@ importers: packages/runtime-bwrap: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol - '@computeragent/runtime-local': + '@open-gitagent/runtime-local': specifier: workspace:* version: link:../runtime-local - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk devDependencies: @@ -325,10 +325,10 @@ importers: packages/runtime-e2b: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk e2b: @@ -377,15 +377,6 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.132 version: 0.2.136(@cfworker/json-schema@4.1.1)(zod@4.4.3) - '@computeragent/protocol': - specifier: workspace:* - version: link:../protocol - '@computeragent/sdk': - specifier: workspace:* - version: link:../sdk - '@computeragent/session-store-mongo': - specifier: workspace:* - version: link:../session-store-mongo '@langchain/anthropic': specifier: ^1.3.0 version: 1.3.29(@langchain/core@1.1.46(openai@6.10.0(ws@8.20.0)(zod@4.4.3))(ws@8.20.0)) @@ -395,6 +386,15 @@ importers: '@langchain/langgraph': specifier: ^1.3.0 version: 1.3.0(@langchain/core@1.1.46(openai@6.10.0(ws@8.20.0)(zod@4.4.3))(ws@8.20.0))(openai@6.10.0(ws@8.20.0)(zod@4.4.3))(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + '@open-gitagent/protocol': + specifier: workspace:* + version: link:../protocol + '@open-gitagent/sdk': + specifier: workspace:* + version: link:../sdk + '@open-gitagent/session-store-mongo': + specifier: workspace:* + version: link:../session-store-mongo deepagents: specifier: ^1.10.2 version: 1.10.2(langsmith@0.7.1(openai@6.10.0(ws@8.20.0)(zod@4.4.3))(ws@8.20.0))(openai@6.10.0(ws@8.20.0)(zod@4.4.3))(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) @@ -432,10 +432,10 @@ importers: packages/runtime-vzvm: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol - '@computeragent/sdk': + '@open-gitagent/sdk': specifier: workspace:* version: link:../sdk node-ssh: @@ -481,7 +481,7 @@ importers: packages/sdk: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol devDependencies: @@ -503,7 +503,7 @@ importers: packages/session-store-mongo: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol mongodb: @@ -519,7 +519,7 @@ importers: packages/session-store-sqlite: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol better-sqlite3: @@ -547,7 +547,7 @@ importers: '@aws-sdk/client-s3': specifier: ^3.700.0 version: 3.1048.0 - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol devDependencies: @@ -560,7 +560,7 @@ importers: packages/task-store-mongo: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol mongodb: @@ -576,7 +576,7 @@ importers: packages/testing: dependencies: - '@computeragent/protocol': + '@open-gitagent/protocol': specifier: workspace:* version: link:../protocol devDependencies: diff --git a/scripts/seed-agent-registry.ts b/scripts/seed-agent-registry.ts index 841197e..3cf8e16 100644 --- a/scripts/seed-agent-registry.ts +++ b/scripts/seed-agent-registry.ts @@ -14,7 +14,7 @@ * Safe to re-run: `register()` is an idempotent upsert by name. */ import { MongoClient } from "mongodb"; -import { AgentRegistry } from "@computeragent/agent-registry-mongo"; +import { AgentRegistry } from "@open-gitagent/agent-registry-mongo"; interface SeedAgent { readonly name: string; From b37f5581908225a6f91f483e467a339f2b44509c Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 08:40:22 -0400 Subject: [PATCH 8/9] fix(agentos-api): chat-sandbox + /run fall back to Mongo registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously /agents/:name/chat-sandbox and /agents/:name/run looked up the target only in the in-memory list (opts.agents). Agents registered via the dashboard or via MongoTelemetry returned 404 UNKNOWN_AGENT — even though the dashboard listed them and showed their logs. Adds a resolveAgent() helper that tries in-memory first, then reads from agent_registry. Registry agents have no envs/gitToken persisted; the server's own env (forwarded by inheritEssentialHostEnv) supplies ANTHROPIC_API_KEY etc, so the harness boots normally. Smoke verified against the deployed enterprise.clawagent.sh: registering a new agent + clicking chat now returns HTTP 200 with a valid sandboxId. --- examples/agentos-api.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/examples/agentos-api.ts b/examples/agentos-api.ts index a9e73e0..7994148 100644 --- a/examples/agentos-api.ts +++ b/examples/agentos-api.ts @@ -421,8 +421,32 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { // Builds the SAME sandbox config the Slack flow uses (Lyzr model, envs, // gitToken) server-side. Pass an existing sessionId to resume that thread's // conversation memory; otherwise a fresh console session is minted. + // Look up an agent by name — in-memory first, then the Mongo registry. + // Registry agents have no auth/envs persisted; the server's own env (forwarded + // via inheritEssentialHostEnv) provides ANTHROPIC_API_KEY etc. + async function resolveAgent(name: string): Promise { + const inMem = byName.get(name); + if (inMem) return inMem; + try { + const doc = await (await registryColl()).findOne({ _id: name }); + if (!doc) return undefined; + const srcStr = typeof doc.source === "string" + ? doc.source + : (doc.source as { url?: string; path?: string })?.url ?? (doc.source as { path?: string })?.path ?? ""; + return { + name: doc._id, + label: doc.label ?? doc._id, + harness: doc.harness ?? "claude-agent-sdk", + source: srcStr, + model: doc.model, + }; + } catch { + return undefined; + } + } + app.post("/agentos/api/agents/:name/chat-sandbox", async (c) => { - const agent = byName.get(c.req.param("name")); + const agent = await resolveAgent(c.req.param("name")); if (!agent) return c.json({ error: { code: "UNKNOWN_AGENT" } }, 404); if (!sandboxCapable(agent.harness)) { return c.json({ error: { code: "NO_SANDBOX", message: `${agent.label} runs one-shot — use /run` } }, 400); @@ -450,7 +474,7 @@ export function createAgentOSApp(opts: AgentOSOptions): Hono { // Streams a fresh POST /run back to the browser. No conversation memory // across turns — each message is an independent run. app.post("/agentos/api/agents/:name/run", async (c) => { - const agent = byName.get(c.req.param("name")); + const agent = await resolveAgent(c.req.param("name")); if (!agent) return c.json({ error: { code: "UNKNOWN_AGENT" } }, 404); const body = await c.req.json().catch(() => ({})) as { message?: string }; const runBody: Record = { From 67426e40621251cc6291206221c725c7fe720267 Mon Sep 17 00:00:00 2001 From: shreyas-lyzr Date: Thu, 28 May 2026 09:21:11 -0400 Subject: [PATCH 9/9] fix(agentos): registry agents are fully chat-capable + transcripts work end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E-tested against prod (enterprise.clawagent.sh): 16/18 checks pass; 2 "failures" are SSE-parser bugs in the test harness, not product bugs (transcript-has-Rohan PASSED on the same run that flagged multi-turn-memory). Backend (examples/agentos-api.ts): - sandboxCapable now depends on harness only, not origin. Registry agents with harness=claude-agent-sdk/gitagent now boot warm sandboxes instead of falling through to one-shot /run. - resolveAgent() helper makes chat-sandbox + /run fall back to the Mongo agent_registry when the name isn't in the in-memory list. - chat_pins collection: maps agent → current dashboard sessionId. chat-sandbox reuses the pin on subsequent boots so conversation memory persists across browser refreshes + sandbox restarts. DELETE /agents/:name/chat-pin clears it (used by "New chat"). - slack_threads now also gets a row written for every web chat-sandbox boot with channel="web", so /sessions and /agents sessionCount surface web chats uniformly with Slack chats. - /sessions/:id handles two harness storage shapes: gitagent (sessions._id = sessionId) and claude-agent-sdk (sessions._id = UUID, sessionId embedded in projectKey via $regex). - Entry extraction normalizes both schemas: gitagent {text} and claude-agent-sdk {message:{role, content:string|[{type:"text",text}]}}. Filters out queue-operation meta events. Frontend (agentos/src): - App.tsx: sidebar w-72 → w-80 for more name room; TypeBadge gets whitespace-nowrap + max-w-[7.5rem] + shrink-0 so harness chips don't wrap to two lines; name span gets min-w-0 flex-1 for proper truncation. - SourceBadge: stops wrapping the whole agent card in . Source URL is now plain text inside the card (which is itself a