Compare commits
2 Commits
main
...
fix/format
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38841c71e0 | ||
|
|
ec1b430a2c |
@ -107,6 +107,7 @@
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
|
||||
- Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema.
|
||||
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/main/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
||||
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
|
||||
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||
|
||||
@ -50,6 +50,8 @@
|
||||
|
||||
### Fixes
|
||||
- WhatsApp: default response prefix only for self-chat, using identity name when set.
|
||||
- Auth: merge main auth profiles into per-agent stores for sub-agents and document inheritance. (#1013) — thanks @marcmarg.
|
||||
- Agents: avoid JSON Schema `format` collisions in tool params by renaming snapshot format fields. (#1013) — thanks @marcmarg.
|
||||
- Fix: make `clawdbot update` auto-update global installs when installed via a package manager.
|
||||
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
||||
- Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
|
||||
|
||||
@ -43,6 +43,15 @@ Auto-archive:
|
||||
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
||||
- `runTimeoutSeconds` does **not** auto-archive; it only stops the run. The session remains until auto-archive.
|
||||
|
||||
## Authentication
|
||||
|
||||
Sub-agent auth is resolved by **agent id**, not by session type:
|
||||
- The sub-agent session key is `agent:<agentId>:subagent:<uuid>`.
|
||||
- The auth store is loaded from that agent’s `agentDir`.
|
||||
- The main agent’s auth profiles are merged in as a **fallback**; agent profiles override main profiles on conflicts.
|
||||
|
||||
Note: the merge is additive, so main profiles are always available as fallbacks. Fully isolated auth per agent is not supported yet.
|
||||
|
||||
## Announce
|
||||
|
||||
Sub-agents report back via an announce step:
|
||||
|
||||
@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
|
||||
|
||||
describe("ensureAuthProfileStore", () => {
|
||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||
@ -45,4 +46,80 @@ describe("ensureAuthProfileStore", () => {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("merges main auth profiles into agent store and keeps agent overrides", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-merge-"));
|
||||
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
try {
|
||||
const mainDir = path.join(root, "main-agent");
|
||||
const agentDir = path.join(root, "agent-x");
|
||||
fs.mkdirSync(mainDir, { recursive: true });
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
process.env.CLAWDBOT_AGENT_DIR = mainDir;
|
||||
process.env.PI_CODING_AGENT_DIR = mainDir;
|
||||
|
||||
const mainStore = {
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "main-key",
|
||||
},
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "main-anthropic-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(mainDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(mainStore, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const agentStore = {
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "agent-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(agentStore, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
expect(store.profiles["anthropic:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "main-anthropic-key",
|
||||
});
|
||||
expect(store.profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "agent-key",
|
||||
});
|
||||
} finally {
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.CLAWDBOT_AGENT_DIR;
|
||||
} else {
|
||||
process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
if (previousPiAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
}
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -111,6 +111,34 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
||||
};
|
||||
}
|
||||
|
||||
function mergeRecord<T>(
|
||||
base?: Record<string, T>,
|
||||
override?: Record<string, T>,
|
||||
): Record<string, T> | undefined {
|
||||
if (!base && !override) return undefined;
|
||||
if (!base) return { ...override };
|
||||
if (!override) return { ...base };
|
||||
return { ...base, ...override };
|
||||
}
|
||||
|
||||
function mergeAuthProfileStores(base: AuthProfileStore, override: AuthProfileStore): AuthProfileStore {
|
||||
if (
|
||||
Object.keys(override.profiles).length === 0 &&
|
||||
!override.order &&
|
||||
!override.lastGood &&
|
||||
!override.usageStats
|
||||
) {
|
||||
return base;
|
||||
}
|
||||
return {
|
||||
version: Math.max(base.version, override.version ?? base.version),
|
||||
profiles: { ...base.profiles, ...override.profiles },
|
||||
order: mergeRecord(base.order, override.order),
|
||||
lastGood: mergeRecord(base.lastGood, override.lastGood),
|
||||
usageStats: mergeRecord(base.usageStats, override.usageStats),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
||||
const oauthPath = resolveOAuthPath();
|
||||
const oauthRaw = loadJsonFile(oauthPath);
|
||||
@ -191,7 +219,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
||||
return store;
|
||||
}
|
||||
|
||||
export function ensureAuthProfileStore(
|
||||
function loadAuthProfileStoreForAgent(
|
||||
agentDir?: string,
|
||||
options?: { allowKeychainPrompt?: boolean },
|
||||
): AuthProfileStore {
|
||||
@ -207,6 +235,19 @@ export function ensureAuthProfileStore(
|
||||
return asStore;
|
||||
}
|
||||
|
||||
// Fallback: inherit auth-profiles from main agent if subagent has none
|
||||
if (agentDir) {
|
||||
const mainAuthPath = resolveAuthStorePath(); // without agentDir = main
|
||||
const mainRaw = loadJsonFile(mainAuthPath);
|
||||
const mainStore = coerceAuthStore(mainRaw);
|
||||
if (mainStore && Object.keys(mainStore.profiles).length > 0) {
|
||||
// Clone main store to subagent directory for auth inheritance
|
||||
saveJsonFile(authPath, mainStore);
|
||||
log.info("inherited auth-profiles from main agent", { agentDir });
|
||||
return mainStore;
|
||||
}
|
||||
}
|
||||
|
||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
||||
const legacy = coerceLegacyStore(legacyRaw);
|
||||
const store: AuthProfileStore = {
|
||||
@ -274,6 +315,21 @@ export function ensureAuthProfileStore(
|
||||
return store;
|
||||
}
|
||||
|
||||
export function ensureAuthProfileStore(
|
||||
agentDir?: string,
|
||||
options?: { allowKeychainPrompt?: boolean },
|
||||
): AuthProfileStore {
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, options);
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const mainAuthPath = resolveAuthStorePath();
|
||||
if (!agentDir || authPath === mainAuthPath) {
|
||||
return store;
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
}
|
||||
|
||||
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const payload = {
|
||||
|
||||
@ -129,16 +129,16 @@ describe("createClawdbotCodingTools", () => {
|
||||
expect(Array.isArray(action?.enum)).toBe(true);
|
||||
expect(action?.enum).toContain("act");
|
||||
|
||||
const format = parameters.properties?.format as
|
||||
const snapshotFormat = parameters.properties?.snapshotFormat as
|
||||
| {
|
||||
type?: unknown;
|
||||
enum?: unknown[];
|
||||
anyOf?: unknown[];
|
||||
}
|
||||
| undefined;
|
||||
expect(format?.type).toBe("string");
|
||||
expect(format?.anyOf).toBeUndefined();
|
||||
expect(format?.enum).toEqual(["aria", "ai"]);
|
||||
expect(snapshotFormat?.type).toBe("string");
|
||||
expect(snapshotFormat?.anyOf).toBeUndefined();
|
||||
expect(snapshotFormat?.enum).toEqual(["aria", "ai"]);
|
||||
});
|
||||
it("inlines local $ref before removing unsupported keywords", () => {
|
||||
const cleaned = __testing.cleanToolSchemaForGemini({
|
||||
|
||||
@ -91,7 +91,7 @@ export const BrowserToolSchema = Type.Object({
|
||||
limit: Type.Optional(Type.Number()),
|
||||
maxChars: Type.Optional(Type.Number()),
|
||||
mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
|
||||
format: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
||||
snapshotFormat: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
||||
refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
|
||||
interactive: Type.Optional(Type.Boolean()),
|
||||
compact: Type.Optional(Type.Boolean()),
|
||||
|
||||
@ -191,8 +191,8 @@ export function createBrowserTool(opts?: {
|
||||
}
|
||||
case "snapshot": {
|
||||
const format =
|
||||
params.format === "ai" || params.format === "aria"
|
||||
? (params.format as "ai" | "aria")
|
||||
params.snapshotFormat === "ai" || params.snapshotFormat === "aria"
|
||||
? (params.snapshotFormat as "ai" | "aria")
|
||||
: "ai";
|
||||
const mode = params.mode === "efficient" ? "efficient" : undefined;
|
||||
const labels = typeof params.labels === "boolean" ? params.labels : undefined;
|
||||
|
||||
@ -40,7 +40,7 @@ const CanvasToolSchema = Type.Object({
|
||||
// eval
|
||||
javaScript: Type.Optional(Type.String()),
|
||||
// snapshot
|
||||
format: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS),
|
||||
outputFormat: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS),
|
||||
maxWidth: Type.Optional(Type.Number()),
|
||||
quality: Type.Optional(Type.Number()),
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
@ -127,7 +127,7 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "snapshot": {
|
||||
const formatRaw = typeof params.format === "string" ? params.format.toLowerCase() : "png";
|
||||
const formatRaw = typeof params.outputFormat === "string" ? params.outputFormat.toLowerCase() : "png";
|
||||
const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
|
||||
const maxWidth =
|
||||
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user