feat: add public profile support

- New KV prefix public:{agent_id}:{key} for public profile data
- Any authenticated user can read all profiles, write only to own
- Worker routes: GET /profiles, GET/PUT/DELETE /profile/:agent_id/:key
- CLI commands: cfg profile, cfg profile <id>, cfg profile set/unset, cfg profiles
This commit is contained in:
团子 2026-05-09 03:19:13 +00:00
parent a3b3c598ab
commit 0ec917bedd
3 changed files with 207 additions and 0 deletions

View File

@ -12,6 +12,10 @@ import {
cmdAdminListAgents, cmdAdminListAgents,
cmdAdminRefreshToken, cmdAdminRefreshToken,
cmdAdminInspect, cmdAdminInspect,
cmdProfiles,
cmdProfile,
cmdProfileSet,
cmdProfileUnset,
showHelp, showHelp,
} from "./commands.js"; } from "./commands.js";
@ -53,6 +57,21 @@ switch (cmd) {
} }
cmdToken(args[0]); cmdToken(args[0]);
break; 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": { case "admin": {
const [sub, ...subArgs] = args; const [sub, ...subArgs] = args;
switch (sub) { switch (sub) {

View File

@ -303,6 +303,102 @@ export async function cmdAdminInspect(args: string[]): Promise<void> {
console.log(`\nTotal: ${keys.length} keys (agent: ${data.agent_id})`); console.log(`\nTotal: ${keys.length} keys (agent: ${data.agent_id})`);
} }
// ── Profile commands ──────────────────────────────────────────────────
export async function cmdProfiles(): Promise<void> {
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<void> {
// 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<void> {
if (args.length < 2) {
console.error("Usage: cfg profile set <KEY> <VALUE>");
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<void> {
if (!args[0]) {
console.error("Usage: cfg profile unset <KEY>");
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 { export function showHelp(): void {
console.log(`cfg — config.shazhou.work CLI console.log(`cfg — config.shazhou.work CLI
@ -323,6 +419,13 @@ Usage:
cfg list List all keys with scope (from cache) cfg list List all keys with scope (from cache)
cfg token <TOKEN> Save auth token cfg token <TOKEN> Save auth token
Profile (public, readable by all agents):
cfg profile Show your own public profile
cfg profile <AGENT_ID> Show another agent's public profile
cfg profile set <KEY> <VALUE> Set a key in your public profile
cfg profile unset <KEY> Delete a key from your public profile
cfg profiles List all agents with public profiles
Admin: Admin:
cfg admin agents List all agents cfg admin agents List all agents
cfg admin add <ID> [--admin] Create agent token (default role: agent) cfg admin add <ID> [--admin] Create agent token (default role: agent)

View File

@ -198,6 +198,61 @@ async function handleSync(auth: AuthInfo, kv: KVNamespace): Promise<Response> {
return json({ agent_id: auth.agent_id, role: auth.role, secrets: resolved }); 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<Response> {
const list = await kv.list({ prefix: "public:" });
const agentIds = new Set<string>();
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<Response> {
const list = await kv.list({ prefix: `public:${agentId}:` });
const profile: Record<string, { value: string; updated_at: string }> = {};
for (const k of list.keys) {
const key = k.name.replace(`public:${agentId}:`, "");
const entry = await kv.get<KVEntry>(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<Response> {
const entry = await kv.get<KVEntry>(`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<Response> {
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<Response> {
await kv.delete(`public:${auth.agent_id}:${key}`);
return json({ agent_id: auth.agent_id, key, status: "deleted" });
}
// ── Admin endpoints ──────────────────────────────────────────────────── // ── Admin endpoints ────────────────────────────────────────────────────
async function handleAdminCreateToken(auth: AuthInfo, kv: KVNamespace, request: Request): Promise<Response> { async function handleAdminCreateToken(auth: AuthInfo, kv: KVNamespace, request: Request): Promise<Response> {
@ -327,6 +382,36 @@ export default {
return handleList(url.searchParams.get("scope"), auth, env.CONFIG_KV); 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 // Admin routes
if (path === "/admin/token" && method === "POST") { if (path === "/admin/token" && method === "POST") {
return handleAdminCreateToken(auth, env.CONFIG_KV, request); return handleAdminCreateToken(auth, env.CONFIG_KV, request);