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.
|
- 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).
|
- 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 (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.
|
- 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.
|
- 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.
|
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||||
|
|||||||
@ -50,6 +50,8 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- WhatsApp: default response prefix only for self-chat, using identity name when set.
|
- 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: 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: 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.
|
- 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.
|
- 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.
|
- `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
|
## Announce
|
||||||
|
|
||||||
Sub-agents report back via an announce step:
|
Sub-agents report back via an announce step:
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
|
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
|
||||||
|
|
||||||
describe("ensureAuthProfileStore", () => {
|
describe("ensureAuthProfileStore", () => {
|
||||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||||
@ -45,4 +46,80 @@ describe("ensureAuthProfileStore", () => {
|
|||||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
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 {
|
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
||||||
const oauthPath = resolveOAuthPath();
|
const oauthPath = resolveOAuthPath();
|
||||||
const oauthRaw = loadJsonFile(oauthPath);
|
const oauthRaw = loadJsonFile(oauthPath);
|
||||||
@ -191,7 +219,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureAuthProfileStore(
|
function loadAuthProfileStoreForAgent(
|
||||||
agentDir?: string,
|
agentDir?: string,
|
||||||
options?: { allowKeychainPrompt?: boolean },
|
options?: { allowKeychainPrompt?: boolean },
|
||||||
): AuthProfileStore {
|
): AuthProfileStore {
|
||||||
@ -207,6 +235,19 @@ export function ensureAuthProfileStore(
|
|||||||
return asStore;
|
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 legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
||||||
const legacy = coerceLegacyStore(legacyRaw);
|
const legacy = coerceLegacyStore(legacyRaw);
|
||||||
const store: AuthProfileStore = {
|
const store: AuthProfileStore = {
|
||||||
@ -274,6 +315,21 @@ export function ensureAuthProfileStore(
|
|||||||
return store;
|
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 {
|
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||||
const authPath = resolveAuthStorePath(agentDir);
|
const authPath = resolveAuthStorePath(agentDir);
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@ -129,16 +129,16 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(Array.isArray(action?.enum)).toBe(true);
|
expect(Array.isArray(action?.enum)).toBe(true);
|
||||||
expect(action?.enum).toContain("act");
|
expect(action?.enum).toContain("act");
|
||||||
|
|
||||||
const format = parameters.properties?.format as
|
const snapshotFormat = parameters.properties?.snapshotFormat as
|
||||||
| {
|
| {
|
||||||
type?: unknown;
|
type?: unknown;
|
||||||
enum?: unknown[];
|
enum?: unknown[];
|
||||||
anyOf?: unknown[];
|
anyOf?: unknown[];
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
expect(format?.type).toBe("string");
|
expect(snapshotFormat?.type).toBe("string");
|
||||||
expect(format?.anyOf).toBeUndefined();
|
expect(snapshotFormat?.anyOf).toBeUndefined();
|
||||||
expect(format?.enum).toEqual(["aria", "ai"]);
|
expect(snapshotFormat?.enum).toEqual(["aria", "ai"]);
|
||||||
});
|
});
|
||||||
it("inlines local $ref before removing unsupported keywords", () => {
|
it("inlines local $ref before removing unsupported keywords", () => {
|
||||||
const cleaned = __testing.cleanToolSchemaForGemini({
|
const cleaned = __testing.cleanToolSchemaForGemini({
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const BrowserToolSchema = Type.Object({
|
|||||||
limit: Type.Optional(Type.Number()),
|
limit: Type.Optional(Type.Number()),
|
||||||
maxChars: Type.Optional(Type.Number()),
|
maxChars: Type.Optional(Type.Number()),
|
||||||
mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
|
mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
|
||||||
format: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
snapshotFormat: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
||||||
refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
|
refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
|
||||||
interactive: Type.Optional(Type.Boolean()),
|
interactive: Type.Optional(Type.Boolean()),
|
||||||
compact: Type.Optional(Type.Boolean()),
|
compact: Type.Optional(Type.Boolean()),
|
||||||
|
|||||||
@ -191,8 +191,8 @@ export function createBrowserTool(opts?: {
|
|||||||
}
|
}
|
||||||
case "snapshot": {
|
case "snapshot": {
|
||||||
const format =
|
const format =
|
||||||
params.format === "ai" || params.format === "aria"
|
params.snapshotFormat === "ai" || params.snapshotFormat === "aria"
|
||||||
? (params.format as "ai" | "aria")
|
? (params.snapshotFormat as "ai" | "aria")
|
||||||
: "ai";
|
: "ai";
|
||||||
const mode = params.mode === "efficient" ? "efficient" : undefined;
|
const mode = params.mode === "efficient" ? "efficient" : undefined;
|
||||||
const labels = typeof params.labels === "boolean" ? params.labels : undefined;
|
const labels = typeof params.labels === "boolean" ? params.labels : undefined;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const CanvasToolSchema = Type.Object({
|
|||||||
// eval
|
// eval
|
||||||
javaScript: Type.Optional(Type.String()),
|
javaScript: Type.Optional(Type.String()),
|
||||||
// snapshot
|
// snapshot
|
||||||
format: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS),
|
outputFormat: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS),
|
||||||
maxWidth: Type.Optional(Type.Number()),
|
maxWidth: Type.Optional(Type.Number()),
|
||||||
quality: Type.Optional(Type.Number()),
|
quality: Type.Optional(Type.Number()),
|
||||||
delayMs: Type.Optional(Type.Number()),
|
delayMs: Type.Optional(Type.Number()),
|
||||||
@ -127,7 +127,7 @@ export function createCanvasTool(): AnyAgentTool {
|
|||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "snapshot": {
|
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 format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
|
||||||
const maxWidth =
|
const maxWidth =
|
||||||
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user