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:
parent
a3b3c598ab
commit
0ec917bedd
@ -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) {
|
||||
|
||||
@ -303,6 +303,102 @@ export async function cmdAdminInspect(args: string[]): Promise<void> {
|
||||
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 {
|
||||
console.log(`cfg — config.shazhou.work CLI
|
||||
|
||||
@ -323,6 +419,13 @@ Usage:
|
||||
cfg list List all keys with scope (from cache)
|
||||
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:
|
||||
cfg admin agents List all agents
|
||||
cfg admin add <ID> [--admin] Create agent token (default role: agent)
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
// ── 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 ────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user