diff --git a/packages/cfg/src/cli.ts b/packages/cfg/src/cli.ts index 6f8171b..7e13c74 100644 --- a/packages/cfg/src/cli.ts +++ b/packages/cfg/src/cli.ts @@ -12,6 +12,10 @@ import { cmdAdminListAgents, cmdAdminRefreshToken, cmdAdminInspect, + cmdProfiles, + cmdProfile, + cmdProfileSet, + cmdProfileUnset, showHelp, } from "./commands.js"; @@ -53,6 +57,21 @@ switch (cmd) { } cmdToken(args[0]); break; + case "profiles": + await cmdProfiles(); + break; + case "profile": { + const [sub, ...subArgs] = args; + if (sub === "set") { + await cmdProfileSet(subArgs); + } else if (sub === "unset") { + await cmdProfileUnset(subArgs); + } else { + // sub is either an agent_id or undefined (show own) + await cmdProfile(sub); + } + break; + } case "admin": { const [sub, ...subArgs] = args; switch (sub) { diff --git a/packages/cfg/src/commands.ts b/packages/cfg/src/commands.ts index ff3ec88..972e341 100644 --- a/packages/cfg/src/commands.ts +++ b/packages/cfg/src/commands.ts @@ -303,6 +303,102 @@ export async function cmdAdminInspect(args: string[]): Promise { console.log(`\nTotal: ${keys.length} keys (agent: ${data.agent_id})`); } +// ── Profile commands ────────────────────────────────────────────────── + +export async function cmdProfiles(): Promise { + const { data } = await api("GET", "/profiles"); + if (!data.agents?.length) { + console.log("No agents with public profiles"); + return; + } + console.log("Agents with public profiles:"); + for (const agent of data.agents) { + console.log(` ${agent}`); + } +} + +export async function cmdProfile(agentId?: string): Promise { + // If no agent specified, show own profile — need to know own agent_id + const path = agentId + ? `/profile/${encodeURIComponent(agentId)}` + : (() => { + const cache = loadCache(); + if (cache?.agent_id) return `/profile/${encodeURIComponent(cache.agent_id)}`; + return null; + })(); + + if (!path) { + console.error("Cannot determine your agent_id. Run: cfg sync"); + process.exit(1); + } + + const { data } = await api("GET", path); + if (!data.profile || Object.keys(data.profile).length === 0) { + console.log(`No public profile for ${data.agent_id}`); + return; + } + const keys = Object.keys(data.profile).sort(); + const maxLen = Math.max(...keys.map((k) => k.length)); + console.log(`Profile: ${data.agent_id}`); + for (const key of keys) { + const entry = data.profile[key]; + console.log(` ${key.padEnd(maxLen + 2)}${entry.value}`); + } +} + +export async function cmdProfileSet(args: string[]): Promise { + if (args.length < 2) { + console.error("Usage: cfg profile set "); + process.exit(1); + } + const [key, ...rest] = args; + const value = rest.join(" "); + // Need own agent_id for the PUT path + let agentId: string | undefined; + const cache = loadCache(); + if (cache?.agent_id) { + agentId = cache.agent_id; + } else { + // Try sync to get agent_id + const ok = await trySyncRemote(); + if (ok) { + const c = loadCache(); + agentId = c?.agent_id; + } + } + if (!agentId) { + console.error("Cannot determine your agent_id. Run: cfg sync"); + process.exit(1); + } + await api("PUT", `/profile/${encodeURIComponent(agentId)}/${encodeURIComponent(key)}`, { value }); + console.log(`✓ ${key} set in public profile`); +} + +export async function cmdProfileUnset(args: string[]): Promise { + if (!args[0]) { + console.error("Usage: cfg profile unset "); + process.exit(1); + } + const key = args[0]; + let agentId: string | undefined; + const cache = loadCache(); + if (cache?.agent_id) { + agentId = cache.agent_id; + } else { + const ok = await trySyncRemote(); + if (ok) { + const c = loadCache(); + agentId = c?.agent_id; + } + } + if (!agentId) { + console.error("Cannot determine your agent_id. Run: cfg sync"); + process.exit(1); + } + await api("DELETE", `/profile/${encodeURIComponent(agentId)}/${encodeURIComponent(key)}`); + console.log(`✓ ${key} deleted from public profile`); +} + export function showHelp(): void { console.log(`cfg — config.shazhou.work CLI @@ -323,6 +419,13 @@ Usage: cfg list List all keys with scope (from cache) cfg token Save auth token +Profile (public, readable by all agents): + cfg profile Show your own public profile + cfg profile Show another agent's public profile + cfg profile set Set a key in your public profile + cfg profile unset Delete a key from your public profile + cfg profiles List all agents with public profiles + Admin: cfg admin agents List all agents cfg admin add [--admin] Create agent token (default role: agent) diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 8f64a91..06793c0 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -198,6 +198,61 @@ async function handleSync(auth: AuthInfo, kv: KVNamespace): Promise { return json({ agent_id: auth.agent_id, role: auth.role, secrets: resolved }); } +// ── Public Profile endpoints ────────────────────────────────────────── + +// GET /profiles — list all agents that have public profile entries +async function handleListProfiles(kv: KVNamespace): Promise { + const list = await kv.list({ prefix: "public:" }); + const agentIds = new Set(); + for (const k of list.keys) { + // public:{agent_id}:{key} + const parts = k.name.split(":"); + if (parts.length >= 3) agentIds.add(parts[1]); + } + return json({ agents: [...agentIds].sort() }); +} + +// GET /profile/:agent_id — list all public keys for an agent +async function handleGetProfile(agentId: string, kv: KVNamespace): Promise { + const list = await kv.list({ prefix: `public:${agentId}:` }); + const profile: Record = {}; + for (const k of list.keys) { + const key = k.name.replace(`public:${agentId}:`, ""); + const entry = await kv.get(k.name, "json"); + if (entry) { + profile[key] = { value: entry.value, updated_at: entry.updated_at }; + } + } + return json({ agent_id: agentId, profile }); +} + +// GET /profile/:agent_id/:key — read a single public key +async function handleGetProfileKey(agentId: string, key: string, kv: KVNamespace): Promise { + const entry = await kv.get(`public:${agentId}:${key}`, "json"); + if (!entry) return err("not found", 404); + return json({ agent_id: agentId, key, value: entry.value, updated_at: entry.updated_at }); +} + +// PUT /profile/:key — write to own public profile +async function handlePutProfileKey(key: string, auth: AuthInfo, kv: KVNamespace, request: Request): Promise { + const body = await request.json<{ value: string }>(); + if (!body?.value && body?.value !== "") return err("missing 'value' in body"); + const entry: KVEntry = { + value: body.value, + updated_at: new Date().toISOString(), + env: false, + secret: false, + }; + await kv.put(`public:${auth.agent_id}:${key}`, JSON.stringify(entry)); + return json({ agent_id: auth.agent_id, key, status: "ok" }); +} + +// DELETE /profile/:key — delete from own public profile +async function handleDeleteProfileKey(key: string, auth: AuthInfo, kv: KVNamespace): Promise { + await kv.delete(`public:${auth.agent_id}:${key}`); + return json({ agent_id: auth.agent_id, key, status: "deleted" }); +} + // ── Admin endpoints ──────────────────────────────────────────────────── async function handleAdminCreateToken(auth: AuthInfo, kv: KVNamespace, request: Request): Promise { @@ -327,6 +382,36 @@ export default { return handleList(url.searchParams.get("scope"), auth, env.CONFIG_KV); } + // Public profile routes + if (path === "/profiles" && method === "GET") { + return handleListProfiles(env.CONFIG_KV); + } + // GET /profile/:agent_id or GET /profile/:agent_id/:key + if (path.startsWith("/profile/")) { + const profilePath = path.replace("/profile/", ""); + const slashIdx = profilePath.indexOf("/"); + if (slashIdx === -1) { + // /profile/:agent_id + const agentId = decodeURIComponent(profilePath); + if (method === "GET") return handleGetProfile(agentId, env.CONFIG_KV); + return err("method not allowed", 405); + } + const agentId = decodeURIComponent(profilePath.slice(0, slashIdx)); + const key = decodeURIComponent(profilePath.slice(slashIdx + 1)); + switch (method) { + case "GET": + return handleGetProfileKey(agentId, key, env.CONFIG_KV); + case "PUT": + if (agentId !== auth.agent_id) return err("can only write to own profile", 403); + return handlePutProfileKey(key, auth, env.CONFIG_KV, request); + case "DELETE": + if (agentId !== auth.agent_id) return err("can only delete from own profile", 403); + return handleDeleteProfileKey(key, auth, env.CONFIG_KV); + default: + return err("method not allowed", 405); + } + } + // Admin routes if (path === "/admin/token" && method === "POST") { return handleAdminCreateToken(auth, env.CONFIG_KV, request);