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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
taskSignal,
} from './task.ts';
import { formatMountListHuman, toolCall, toolDescribe, toolList, toolMount } from './tool.ts';
import { parseToolBridgeArgs, toolBridgeAdminCall } from './toolbridge.ts';
import { formatWhoamiHuman, whoami } from './whoami.ts';

/** commander 的可重复选项收集器(`--metadata k=v` 累积成数组)。 */
Expand Down Expand Up @@ -951,6 +952,22 @@ export async function run(argv: string[], opts: RunOptions = {}): Promise<number
else out(JSON.stringify(res.result, null, 2));
});

const toolbridge = program
.command('toolbridge')
.description('Call Tool Bridge Admin SDK capabilities through Watt');
toolbridge
.command('call')
.description('Call a Tool Bridge admin method, e.g. ProvidersList or HostsCreate')
.argument('<method>', 'Tool Bridge admin method exposed by /htbp/platform/toolbridge')
.option('--args <json>', 'method arguments as a JSON object', '{}')
.action(async (method: string, cmdOpts: { args: string }) => {
const base = requireBaseUrl(env());
const token = requireToken(env(), credPath, opts.fs);
const args = parseToolBridgeArgs(cmdOpts.args);
const result = await toolBridgeAdminCall(base, token, method, args, { fetch: opts.fetch });
out(JSON.stringify(result, null, 2));
});

// ── watt agent(§3.1 AgentRegistry + §3.2 AgentRuntime)─────────────────────
const agent = program
.command('agent')
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/toolbridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type HttpDeps, htbpCall } from './client.ts';
import { CliError } from './env.ts';

export async function toolBridgeAdminCall(
base: string,
token: string,
tool: string,
args: Record<string, unknown>,
deps: HttpDeps = {},
): Promise<unknown> {
return htbpCall(base, token, 'toolbridge', tool, args, deps);
}

export function parseToolBridgeArgs(raw: string): Record<string, unknown> {
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('not an object');
}
return parsed as Record<string, unknown>;
} catch {
throw new CliError('--args must be a JSON object', 2);
}
}
3 changes: 2 additions & 1 deletion packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"react-dom": "^19.2.0",
"react-router": "^7.9.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"tslib": "^2.8.1"
},
"devDependencies": {
"@react-router/dev": "^7.9.4",
Expand Down
3 changes: 2 additions & 1 deletion packages/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"agents": "^0.17.3",
"ai": "^6.0.219",
"hono": "4.12.27",
"jose": "^6.2.3"
"jose": "^6.2.3",
"@tokenroll/tool-bridge": "^0.1.0"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.18.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/gateway/src/agent/harness/htbp-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function createHtbpTools(deps: HtbpToolsDeps): HarnessTool[] {
}
const result = await executeToolRequest(
env,
{ toolPath: path, op, accept: 'text/plain' },
{ toolPath: path, op, ...(op === 'skill' ? { accept: 'text/plain' } : {}) },
{
trim,
},
Expand Down
11 changes: 11 additions & 0 deletions packages/gateway/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ export interface Bindings {
ANTHROPIC_BASE_URL?: string;
/** lurker scratch namespace TTL 秒(正整数串)——缺省 120(保 E2E 分钟级过期断言);生产设 3600。 */
LURKER_SCRATCH_TTL_SEC?: string;
/** Tool Bridge Host SDK 注册的 host id;缺省 watt。 */
WATT_TOOLBRIDGE_HOST_ID?: string;
/**
* Tool Bridge SDK key。默认只配这一把,用于 Host SDK 同步/调用与 Admin SDK 管理面。
* 若需要最小权限隔离,可改用下面两个分离 key。
*/
WATT_TOOLBRIDGE_KEY?: string;
/** 可选:仅用于 Host SDK mounts.sync 与 /htbp 调用的 S2S key。 */
WATT_TOOLBRIDGE_HOST_KEY?: string;
/** 可选:仅用于 /htbp/platform/toolbridge 管理面的 Admin SDK key。 */
WATT_TOOLBRIDGE_ADMIN_KEY?: string;
/** Root Key 的 SHA-256 摘要(hex,§6.5e)——明文永不进平台;缺省则 /oauth/root/token 报未启用。
* 设置:scripts/set-root-key.mjs(生成/覆写,明文仅展示一次)或 watt init 向导。 */
WATT_ROOT_KEY_HASH?: string;
Expand Down
39 changes: 39 additions & 0 deletions packages/gateway/src/http/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { SecretStore } from '../secrets/secret-store.ts';
import { defaultManagerDeps, TaskManager } from '../task/task-manager.ts';
import type { ListOptions as TaskListOptions } from '../task/task-store.ts';
import { type ListOptions as ToolListOptions, ToolRegistry } from '../tools/tool-registry.ts';
import { executeToolBridgeAdmin, TOOLBRIDGE_ADMIN_ACTIONS } from '../tools/toolbridge-admin.ts';
import { type AuthVars, authMiddleware } from './auth.ts';
import { forbiddenResponse, wattErrorResponse } from './errors.ts';

Expand All @@ -82,6 +83,7 @@ const RES_EVENT = 'platform://event';
const RES_CHANNEL = 'platform://channel';
const RES_CONTEXT = 'platform://context';
const RES_TOOL = 'platform://tool';
const RES_TOOLBRIDGE = 'platform://toolbridge';
const RES_AGENT = 'platform://agent';
const RES_PROVIDER = 'platform://provider';
const RES_TASK = 'platform://task';
Expand Down Expand Up @@ -784,6 +786,43 @@ export function platformRoutes(): Hono<{ Bindings: Bindings; Variables: AuthVars
}
});

// ── POST /htbp/platform/toolbridge → Tool Bridge Admin SDK 管理面 ───────
// 覆盖 Tool Bridge SDK 管理能力:providers/publications/placements/hosts/endpoints/
// command policies/audit/legacy servers/bridge/tree。Watt 只做平台鉴权与错误形状转换,
// 真实操作通过 Admin SDK 走 TOOLBRIDGE service binding。
app.post('/htbp/platform/toolbridge', async (c) => {
const claims = c.get('claims');
const authorizer = newAuthorizer(c.env, c.get('callContext').traceId);

let call: HtbpCall;
try {
call = (await c.req.json()) as HtbpCall;
} catch {
return wattErrorResponse(
c,
wattError('invalid_argument', 'request body must be JSON', false),
);
}
const tool = call.tool;
const args = call.arguments ?? {};

const action = tool ? TOOLBRIDGE_ADMIN_ACTIONS[tool] : undefined;
if (action === undefined) {
return wattErrorResponse(
c,
wattError('invalid_argument', `unknown tool: ${String(tool)}`, false),
);
}
const decision = await authorizer.check(claims, RES_TOOLBRIDGE, action);
if (!decision.allow) {
return forbiddenResponse(c, decision.reason ?? 'access denied on platform://toolbridge');
}

const result = await executeToolBridgeAdmin(c.env, tool as string, args);
if (isWattError(result)) return wattErrorResponse(c, result);
return c.json(result);
});

// ── POST /htbp/platform/agent → AgentRegistry(§3.1)+ AgentRuntime(§3.2)────────
// Registry 动词:List/Get/Write/Update;Runtime 动词:Spawn/Send/Status/Terminate/ListInstances。
// tool → action(§6.4d):读=read(List/Get/Status/ListInstances);
Expand Down
Loading
Loading