File size: 7,277 Bytes
fb4d8fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
  ANTHROPIC_SETUP_TOKEN_PREFIX,
  validateAnthropicSetupToken,
} from "../commands/auth-token.js";
import { loadConfig } from "../config/config.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import {
  type AuthProfileCredential,
  ensureAuthProfileStore,
  saveAuthProfileStore,
} from "./auth-profiles.js";
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
import { normalizeProviderId, parseModelRef } from "./model-selection.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";

const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST);
const SETUP_TOKEN_RAW = process.env.OPENCLAW_LIVE_SETUP_TOKEN?.trim() ?? "";
const SETUP_TOKEN_VALUE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
const SETUP_TOKEN_PROFILE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";
const SETUP_TOKEN_MODEL = process.env.OPENCLAW_LIVE_SETUP_TOKEN_MODEL?.trim() ?? "";

const ENABLED = LIVE && Boolean(SETUP_TOKEN_RAW || SETUP_TOKEN_VALUE || SETUP_TOKEN_PROFILE);
const describeLive = ENABLED ? describe : describe.skip;

type TokenSource = {
  agentDir: string;
  profileId: string;
  cleanup?: () => Promise<void>;
};

function isSetupToken(value: string): boolean {
  return value.startsWith(ANTHROPIC_SETUP_TOKEN_PREFIX);
}

function listSetupTokenProfiles(store: {
  profiles: Record<string, AuthProfileCredential>;
}): string[] {
  return Object.entries(store.profiles)
    .filter(([, cred]) => {
      if (cred.type !== "token") {
        return false;
      }
      if (normalizeProviderId(cred.provider) !== "anthropic") {
        return false;
      }
      return isSetupToken(cred.token);
    })
    .map(([id]) => id);
}

function pickSetupTokenProfile(candidates: string[]): string {
  const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"];
  for (const id of preferred) {
    if (candidates.includes(id)) {
      return id;
    }
  }
  return candidates[0] ?? "";
}

async function resolveTokenSource(): Promise<TokenSource> {
  const explicitToken =
    (SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE;

  if (explicitToken) {
    const error = validateAnthropicSetupToken(explicitToken);
    if (error) {
      throw new Error(`Invalid setup-token: ${error}`);
    }
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-setup-token-"));
    const profileId = `anthropic:setup-token-live-${randomUUID()}`;
    const store = ensureAuthProfileStore(tempDir, {
      allowKeychainPrompt: false,
    });
    store.profiles[profileId] = {
      type: "token",
      provider: "anthropic",
      token: explicitToken,
    };
    saveAuthProfileStore(store, tempDir);
    return {
      agentDir: tempDir,
      profileId,
      cleanup: async () => {
        await fs.rm(tempDir, { recursive: true, force: true });
      },
    };
  }

  const agentDir = resolveOpenClawAgentDir();
  const store = ensureAuthProfileStore(agentDir, {
    allowKeychainPrompt: false,
  });

  const candidates = listSetupTokenProfiles(store);
  if (SETUP_TOKEN_PROFILE) {
    if (!candidates.includes(SETUP_TOKEN_PROFILE)) {
      const available = candidates.length > 0 ? candidates.join(", ") : "(none)";
      throw new Error(
        `Setup-token profile "${SETUP_TOKEN_PROFILE}" not found. Available: ${available}.`,
      );
    }
    return { agentDir, profileId: SETUP_TOKEN_PROFILE };
  }

  if (SETUP_TOKEN_RAW && SETUP_TOKEN_RAW !== "1" && SETUP_TOKEN_RAW !== "auto") {
    throw new Error(
      "OPENCLAW_LIVE_SETUP_TOKEN did not look like a setup-token. Use OPENCLAW_LIVE_SETUP_TOKEN_VALUE for raw tokens.",
    );
  }

  if (candidates.length === 0) {
    throw new Error(
      "No Anthropics setup-token profiles found. Set OPENCLAW_LIVE_SETUP_TOKEN_VALUE or OPENCLAW_LIVE_SETUP_TOKEN_PROFILE.",
    );
  }
  return { agentDir, profileId: pickSetupTokenProfile(candidates) };
}

function pickModel(models: Array<Model<Api>>, raw?: string): Model<Api> | null {
  const normalized = raw?.trim() ?? "";
  if (normalized) {
    const parsed = parseModelRef(normalized, "anthropic");
    if (!parsed) {
      return null;
    }
    return (
      models.find(
        (model) =>
          normalizeProviderId(model.provider) === parsed.provider && model.id === parsed.model,
      ) ?? null
    );
  }

  const preferred = [
    "claude-opus-4-5",
    "claude-sonnet-4-5",
    "claude-sonnet-4-0",
    "claude-haiku-3-5",
  ];
  for (const id of preferred) {
    const match = models.find((model) => model.id === id);
    if (match) {
      return match;
    }
  }
  return models[0] ?? null;
}

describeLive("live anthropic setup-token", () => {
  it(
    "completes using a setup-token profile",
    async () => {
      const tokenSource = await resolveTokenSource();
      try {
        const cfg = loadConfig();
        await ensureOpenClawModelsJson(cfg, tokenSource.agentDir);

        const authStorage = discoverAuthStorage(tokenSource.agentDir);
        const modelRegistry = discoverModels(authStorage, tokenSource.agentDir);
        const all = Array.isArray(modelRegistry) ? modelRegistry : modelRegistry.getAll();
        const candidates = all.filter(
          (model) => normalizeProviderId(model.provider) === "anthropic",
        ) as Array<Model<Api>>;
        expect(candidates.length).toBeGreaterThan(0);

        const model = pickModel(candidates, SETUP_TOKEN_MODEL);
        if (!model) {
          throw new Error(
            SETUP_TOKEN_MODEL
              ? `Model not found: ${SETUP_TOKEN_MODEL}`
              : "No Anthropic models available.",
          );
        }

        const apiKeyInfo = await getApiKeyForModel({
          model,
          cfg,
          profileId: tokenSource.profileId,
          agentDir: tokenSource.agentDir,
        });
        const apiKey = requireApiKey(apiKeyInfo, model.provider);
        const tokenError = validateAnthropicSetupToken(apiKey);
        if (tokenError) {
          throw new Error(`Resolved profile is not a setup-token: ${tokenError}`);
        }

        const res = await completeSimple(
          model,
          {
            messages: [
              {
                role: "user",
                content: "Reply with the word ok.",
                timestamp: Date.now(),
              },
            ],
          },
          {
            apiKey,
            maxTokens: 64,
            temperature: 0,
          },
        );
        const text = res.content
          .filter((block) => block.type === "text")
          .map((block) => block.text.trim())
          .join(" ");
        expect(text.toLowerCase()).toContain("ok");
      } finally {
        if (tokenSource.cleanup) {
          await tokenSource.cleanup();
        }
      }
    },
    5 * 60 * 1000,
  );
});