@inneranimalmedia/agentsam-sdk 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/AUTH_IDENTITY_CONTRACT.md +25 -27
- package/docs/SDK_WORKER.md +4 -3
- package/package.json +1 -1
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/contracts/auth-config.js +23 -40
- package/packages/identity/tests/auth-config.test.mjs +14 -23
- package/src/cli.js +13 -23
- package/src/commands/account-auth.js +1 -1
- package/src/commands/deploy.js +2 -2
- package/src/commands/providers.js +308 -0
- package/src/commands/shell.js +4 -0
- package/src/commands/tunnel.js +8 -8
- package/src/commands/whoami.js +42 -21
- package/src/errors/diagnostic.js +1 -0
- package/src/lib/account-session.js +94 -25
- package/src/lib/auth.js +314 -51
- package/src/lib/core-client.js +75 -30
- package/src/lib/detect-context.js +14 -16
- package/src/lib/provider-credentials.js +171 -55
- package/src/lib/slash-commands.js +1 -0
- package/src/models/discovery.js +35 -0
- package/src/ui/cli/help.js +3 -1
- package/test/account-session.test.mjs +48 -11
- package/test/apps-scaffold-contract.test.mjs +43 -0
- package/test/sdk-worker-contract.test.mjs +5 -1
- package/test/smoke.mjs +2 -1
- package/test/whoami-resume.test.mjs +6 -5
- package/src/lib/prompt-byok.js +0 -57
- package/src/lib/save-sdk-token.js +0 -19
|
@@ -2,15 +2,41 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
const PROVIDER_CREDENTIALS = Object.freeze({
|
|
6
|
-
openai: Object.freeze({ env: 'OPENAI_API_KEY', files: ['openai.env'] }),
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
export const PROVIDER_CREDENTIALS = Object.freeze({
|
|
6
|
+
openai: Object.freeze({ label: 'OpenAI', env: 'OPENAI_API_KEY', files: ['openai.env'], modelProvider: 'openai' }),
|
|
7
|
+
anthropic: Object.freeze({ label: 'Anthropic', env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'], modelProvider: 'anthropic' }),
|
|
8
|
+
gemini: Object.freeze({ label: 'Gemini', env: 'GEMINI_API_KEY', files: ['gemini.env'], modelProvider: 'gemini' }),
|
|
9
|
+
cursor: Object.freeze({ label: 'Cursor', env: 'CURSOR_API_KEY', files: ['cursor.env'], modelProvider: 'cursor' }),
|
|
10
|
+
xai: Object.freeze({ label: 'xAI', env: 'XAI_API_KEY', files: ['xai.env', 'grok.env'], modelProvider: 'xai' }),
|
|
11
|
+
cloudflare: Object.freeze({
|
|
12
|
+
label: 'Cloudflare',
|
|
13
|
+
env: 'CLOUDFLARE_API_TOKEN',
|
|
14
|
+
files: ['cloudflare.env'],
|
|
15
|
+
modelProvider: 'cloudflare',
|
|
16
|
+
accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'],
|
|
17
|
+
}),
|
|
18
|
+
inneranimalmedia: Object.freeze({
|
|
19
|
+
label: 'InnerAnimalMedia',
|
|
20
|
+
env: 'AGENTSAM_API_KEY',
|
|
21
|
+
files: ['inneranimalmedia.env'],
|
|
22
|
+
tokenPrefix: 'aak_',
|
|
23
|
+
platformCredential: true,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const PROVIDER_ALIASES = Object.freeze({
|
|
28
|
+
grok: 'xai',
|
|
29
|
+
iam: 'inneranimalmedia',
|
|
30
|
+
inneranimal: 'inneranimalmedia',
|
|
11
31
|
});
|
|
12
32
|
|
|
13
33
|
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
34
|
+
|
|
35
|
+
export function normalizeProviderId(provider) {
|
|
36
|
+
const id = clean(provider).toLowerCase();
|
|
37
|
+
return PROVIDER_ALIASES[id] || id;
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
function normalizeCloudflareAccountId(value) {
|
|
15
41
|
const id = clean(value);
|
|
16
42
|
if (!id) return '';
|
|
@@ -18,6 +44,16 @@ function normalizeCloudflareAccountId(value) {
|
|
|
18
44
|
return id;
|
|
19
45
|
}
|
|
20
46
|
|
|
47
|
+
function validateCredentialValue(spec, value) {
|
|
48
|
+
const secret = clean(value);
|
|
49
|
+
if (!secret) throw new Error('credential_required');
|
|
50
|
+
if (/[\r\n\0]/.test(secret)) throw new Error('credential_must_be_single_line');
|
|
51
|
+
if (spec.tokenPrefix && !secret.startsWith(spec.tokenPrefix)) {
|
|
52
|
+
throw new Error(`credential_prefix_required:${spec.tokenPrefix}`);
|
|
53
|
+
}
|
|
54
|
+
return secret;
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
function homeDirectory(options = {}) {
|
|
22
58
|
return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
23
59
|
}
|
|
@@ -58,6 +94,23 @@ function firstRuntimeValue(env, names = []) {
|
|
|
58
94
|
return '';
|
|
59
95
|
}
|
|
60
96
|
|
|
97
|
+
function atomicWrite(filename, source, mode = 0o600) {
|
|
98
|
+
const dir = path.dirname(filename);
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
100
|
+
if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
|
|
101
|
+
const temp = `${filename}.${process.pid}.${Date.now()}.tmp`;
|
|
102
|
+
fs.writeFileSync(temp, source, { mode });
|
|
103
|
+
if (process.platform !== 'win32') fs.chmodSync(temp, mode);
|
|
104
|
+
fs.renameSync(temp, filename);
|
|
105
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, mode);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function envLiteral(value) {
|
|
109
|
+
const text = String(value ?? '');
|
|
110
|
+
if (/['"\r\n\0]/.test(text)) throw new Error('credential_contains_unsupported_profile_character');
|
|
111
|
+
return `"${text}"`;
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
export function agentEnvDirectory(options = {}) {
|
|
62
115
|
return path.join(homeDirectory(options), '.agentsam', 'env.d');
|
|
63
116
|
}
|
|
@@ -68,83 +121,143 @@ export function agentEnvLoaderPath(options = {}) {
|
|
|
68
121
|
|
|
69
122
|
export function ensureAgentEnvLoader(options = {}) {
|
|
70
123
|
const filename = agentEnvLoaderPath(options);
|
|
71
|
-
|
|
124
|
+
const supported = Object.keys(PROVIDER_CREDENTIALS).join('|');
|
|
72
125
|
const source = `# AgentSam provider environment loader. Source this file; do not execute it.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*) echo \"usage: source ~/.agentsam/load-agent-env.sh <openai|anthropic|gemini|grok|cloudflare>\" >&2; return 2 2>/dev/null || exit 2 ;;
|
|
77
|
-
esac
|
|
78
|
-
_agentsam_file=\"\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env\"
|
|
79
|
-
if [ ! -f \"\$_agentsam_file\" ]; then
|
|
80
|
-
echo \"AgentSam provider profile not found: \$_agentsam_file\" >&2
|
|
81
|
-
return 1 2>/dev/null || exit 1
|
|
82
|
-
fi
|
|
83
|
-
set -a
|
|
84
|
-
. \"\$_agentsam_file\"
|
|
85
|
-
set +a
|
|
86
|
-
if [ \"\$_agentsam_profile\" = cloudflare ]; then
|
|
87
|
-
if [ -z \"\${ACCOUNT_ID:-}\" ] && [ -n \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ]; then export ACCOUNT_ID=\"\$CLOUDFLARE_ACCOUNT_ID\"; fi
|
|
88
|
-
if [ -z \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ] && [ -n \"\${ACCOUNT_ID:-}\" ]; then export CLOUDFLARE_ACCOUNT_ID=\"\$ACCOUNT_ID\"; fi
|
|
126
|
+
if [ "$#" -eq 0 ]; then
|
|
127
|
+
echo "usage: source ~/.agentsam/load-agent-env.sh <profile> [profile ...]" >&2
|
|
128
|
+
return 2 2>/dev/null || exit 2
|
|
89
129
|
fi
|
|
130
|
+
for _agentsam_profile in "$@"; do
|
|
131
|
+
case "$_agentsam_profile" in
|
|
132
|
+
grok) _agentsam_profile="xai" ;;
|
|
133
|
+
${supported}) ;;
|
|
134
|
+
*) echo "unknown AgentSam provider profile: $_agentsam_profile" >&2; return 2 2>/dev/null || exit 2 ;;
|
|
135
|
+
esac
|
|
136
|
+
_agentsam_file="\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env"
|
|
137
|
+
if [ ! -f "$_agentsam_file" ]; then
|
|
138
|
+
echo "AgentSam provider profile not found: $_agentsam_file" >&2
|
|
139
|
+
return 1 2>/dev/null || exit 1
|
|
140
|
+
fi
|
|
141
|
+
set -a
|
|
142
|
+
. "$_agentsam_file"
|
|
143
|
+
set +a
|
|
144
|
+
if [ "$_agentsam_profile" = cloudflare ]; then
|
|
145
|
+
if [ -z "\${ACCOUNT_ID:-}" ] && [ -n "\${CLOUDFLARE_ACCOUNT_ID:-}" ]; then export ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID"; fi
|
|
146
|
+
if [ -z "\${CLOUDFLARE_ACCOUNT_ID:-}" ] && [ -n "\${ACCOUNT_ID:-}" ]; then export CLOUDFLARE_ACCOUNT_ID="$ACCOUNT_ID"; fi
|
|
147
|
+
fi
|
|
148
|
+
done
|
|
90
149
|
unset _agentsam_file _agentsam_profile
|
|
91
150
|
`;
|
|
92
|
-
|
|
93
|
-
if (process.platform !== 'win32') fs.chmodSync(filename, 0o700);
|
|
151
|
+
atomicWrite(filename, source, 0o700);
|
|
94
152
|
return filename;
|
|
95
153
|
}
|
|
96
154
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
155
|
+
function profileSource(provider, credential = '', options = {}) {
|
|
156
|
+
const spec = providerCredentialSpec(provider);
|
|
157
|
+
if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
|
|
158
|
+
const lines = [`# AgentSam ${spec.label} provider profile`];
|
|
159
|
+
if (spec.provider === 'cloudflare') {
|
|
160
|
+
lines.push('# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.');
|
|
161
|
+
lines.push(`export ACCOUNT_ID=${envLiteral(normalizeCloudflareAccountId(options.accountId))}`);
|
|
101
162
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
163
|
+
lines.push(`export ${spec.env}=${envLiteral(credential)}`);
|
|
164
|
+
return `${lines.join('\n')}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function providerCredentialSpec(provider) {
|
|
168
|
+
const id = normalizeProviderId(provider);
|
|
169
|
+
const spec = PROVIDER_CREDENTIALS[id];
|
|
170
|
+
return spec ? Object.freeze({ provider: id, ...spec }) : null;
|
|
105
171
|
}
|
|
106
172
|
|
|
107
173
|
export function ensureProviderEnvProfile(provider, options = {}) {
|
|
108
174
|
const spec = providerCredentialSpec(provider);
|
|
109
|
-
if (!spec) throw new Error(`unsupported_provider:${
|
|
175
|
+
if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
|
|
110
176
|
const dir = agentEnvDirectory(options);
|
|
111
177
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
112
178
|
if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
|
|
113
179
|
const filename = path.join(dir, spec.files[0]);
|
|
114
180
|
let created = false;
|
|
115
181
|
if (!fs.existsSync(filename)) {
|
|
116
|
-
|
|
182
|
+
atomicWrite(filename, profileSource(spec.provider, '', options), 0o600);
|
|
117
183
|
created = true;
|
|
118
|
-
} else
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
} else {
|
|
185
|
+
const safety = secureFile(filename);
|
|
186
|
+
if (!safety.ok) throw new Error(safety.error);
|
|
187
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
188
|
+
if (spec.provider === 'cloudflare' && clean(options.accountId)) {
|
|
189
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
190
|
+
const current = firstEnvValue(source, spec.accountEnv || []);
|
|
191
|
+
if (!current) {
|
|
192
|
+
const accountId = normalizeCloudflareAccountId(options.accountId);
|
|
193
|
+
const line = `export ACCOUNT_ID=${envLiteral(accountId)}`;
|
|
194
|
+
const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
|
|
195
|
+
? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
|
|
196
|
+
: `${line}\n${source}`;
|
|
197
|
+
atomicWrite(filename, next, 0o600);
|
|
198
|
+
}
|
|
128
199
|
}
|
|
129
200
|
}
|
|
130
|
-
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
131
201
|
const loader = ensureAgentEnvLoader(options);
|
|
132
|
-
return Object.freeze({
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
provider: spec.provider,
|
|
204
|
+
file: filename,
|
|
205
|
+
loader,
|
|
206
|
+
created,
|
|
207
|
+
source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}`,
|
|
208
|
+
});
|
|
133
209
|
}
|
|
134
210
|
|
|
135
|
-
export function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
211
|
+
export function setProviderCredential(provider, credential, options = {}) {
|
|
212
|
+
const spec = providerCredentialSpec(provider);
|
|
213
|
+
if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
|
|
214
|
+
const value = validateCredentialValue(spec, credential);
|
|
215
|
+
const filename = path.join(agentEnvDirectory(options), spec.files[0]);
|
|
216
|
+
atomicWrite(filename, profileSource(spec.provider, value, options), 0o600);
|
|
217
|
+
const loader = ensureAgentEnvLoader(options);
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
provider: spec.provider,
|
|
220
|
+
file: filename,
|
|
221
|
+
loader,
|
|
222
|
+
source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function removeProviderCredential(provider, options = {}) {
|
|
227
|
+
const spec = providerCredentialSpec(provider);
|
|
228
|
+
if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
|
|
229
|
+
const dir = agentEnvDirectory(options);
|
|
230
|
+
let removed = false;
|
|
231
|
+
for (const basename of spec.files) {
|
|
232
|
+
const filename = path.join(dir, basename);
|
|
233
|
+
if (!fs.existsSync(filename)) continue;
|
|
234
|
+
const safety = secureFile(filename);
|
|
235
|
+
if (!safety.ok) throw new Error(safety.error);
|
|
236
|
+
fs.rmSync(filename, { force: true });
|
|
237
|
+
removed = true;
|
|
238
|
+
}
|
|
239
|
+
return Object.freeze({ provider: spec.provider, removed });
|
|
139
240
|
}
|
|
140
241
|
|
|
141
242
|
export function resolveProviderCredential(provider, options = {}) {
|
|
142
243
|
const spec = providerCredentialSpec(provider);
|
|
143
|
-
if (!spec) return Object.freeze({ provider:
|
|
244
|
+
if (!spec) return Object.freeze({ provider: normalizeProviderId(provider), configured: false, source: null, error: 'unsupported_provider', value: '' });
|
|
144
245
|
const env = options.env || process.env;
|
|
145
246
|
const accountIdFromEnv = firstRuntimeValue(env, spec.accountEnv || []);
|
|
146
247
|
const fromEnv = clean(env?.[spec.env]);
|
|
147
|
-
if (fromEnv)
|
|
248
|
+
if (fromEnv) {
|
|
249
|
+
const prefixError = spec.tokenPrefix && !fromEnv.startsWith(spec.tokenPrefix) ? `credential_prefix_required:${spec.tokenPrefix}` : null;
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
provider: spec.provider,
|
|
252
|
+
configured: !prefixError,
|
|
253
|
+
source: 'environment',
|
|
254
|
+
env: spec.env,
|
|
255
|
+
file: null,
|
|
256
|
+
error: prefixError,
|
|
257
|
+
value: prefixError ? '' : fromEnv,
|
|
258
|
+
account_id: accountIdFromEnv || null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
148
261
|
|
|
149
262
|
const dir = agentEnvDirectory({ ...options, env });
|
|
150
263
|
for (const basename of spec.files) {
|
|
@@ -156,19 +269,22 @@ export function resolveProviderCredential(provider, options = {}) {
|
|
|
156
269
|
const source = fs.readFileSync(filename, 'utf8');
|
|
157
270
|
const value = clean(parseEnvValue(source, spec.env));
|
|
158
271
|
const accountId = accountIdFromEnv || firstEnvValue(source, spec.accountEnv || []);
|
|
159
|
-
|
|
160
|
-
return Object.freeze({ provider: spec.provider, configured:
|
|
272
|
+
const prefixError = spec.tokenPrefix && value && !value.startsWith(spec.tokenPrefix) ? `credential_prefix_required:${spec.tokenPrefix}` : null;
|
|
273
|
+
if (value && !prefixError) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
|
|
274
|
+
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: prefixError || 'credential_variable_missing', value: '' });
|
|
161
275
|
} catch (error) {
|
|
162
276
|
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: error?.message || String(error), value: '' });
|
|
163
277
|
}
|
|
164
278
|
}
|
|
165
|
-
return Object.freeze({ provider: spec.provider, configured: false, source: null, env: spec.env, file: null, error: null, value: '' });
|
|
279
|
+
return Object.freeze({ provider: spec.provider, configured: false, source: null, env: spec.env, file: null, error: null, value: '', account_id: accountIdFromEnv || null });
|
|
166
280
|
}
|
|
167
281
|
|
|
168
282
|
export function describeProviderCredential(provider, options = {}) {
|
|
169
283
|
const resolved = resolveProviderCredential(provider, options);
|
|
284
|
+
const spec = providerCredentialSpec(provider);
|
|
170
285
|
return Object.freeze({
|
|
171
286
|
provider: resolved.provider,
|
|
287
|
+
label: spec?.label || resolved.provider,
|
|
172
288
|
configured: resolved.configured,
|
|
173
289
|
source: resolved.source,
|
|
174
290
|
env: resolved.env || null,
|
|
@@ -11,6 +11,7 @@ export const SLASH_COMMANDS = [
|
|
|
11
11
|
{ cmd: '/context', description: 'Show model context economics; add repo for Git bridge context', lane: 'context' },
|
|
12
12
|
{ cmd: '/status', description: 'Local project, DB, Git, and PTY health', lane: 'local' },
|
|
13
13
|
{ cmd: '/models', description: 'Probe providers and provider-verified known models', lane: 'model' },
|
|
14
|
+
{ cmd: '/providers', description: 'Configure and verify machine provider credentials', lane: 'model' },
|
|
14
15
|
{ cmd: '/login', description: 'Sign in to Inner Animal Media and save the machine-local Agent Sam session', lane: 'identity' },
|
|
15
16
|
{ cmd: '/logout', description: 'Sign out locally without deleting provider credentials', lane: 'identity' },
|
|
16
17
|
{ cmd: '/whoami', description: 'Show authenticated account identity and safe credential status', lane: 'identity' },
|
package/src/models/discovery.js
CHANGED
|
@@ -242,6 +242,40 @@ export async function discoverXaiModels(apiKey, fetchImpl = fetch) {
|
|
|
242
242
|
} catch (error) { return failure(error); }
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
|
|
246
|
+
export async function discoverCursorModels(apiKey, fetchImpl = fetch) {
|
|
247
|
+
if (!clean(apiKey)) return failure('credential unavailable', false);
|
|
248
|
+
try {
|
|
249
|
+
const body = await fetchJson(fetchImpl, 'https://api.cursor.com/v1/models', {
|
|
250
|
+
headers: { authorization: `Bearer ${clean(apiKey)}` },
|
|
251
|
+
});
|
|
252
|
+
const models = (Array.isArray(body?.items) ? body.items : [])
|
|
253
|
+
.map((row) => {
|
|
254
|
+
const id = clean(row?.id);
|
|
255
|
+
if (!id) return null;
|
|
256
|
+
const params = Array.isArray(row?.parameters) ? row.parameters : [];
|
|
257
|
+
const reasoning = params.find((param) => /reason|thinking/i.test(clean(param?.id)));
|
|
258
|
+
const reasoningEfforts = Array.isArray(reasoning?.values)
|
|
259
|
+
? reasoning.values.map((entry) => clean(entry?.value)).filter(Boolean)
|
|
260
|
+
: [];
|
|
261
|
+
return baseRecord('cursor', id, {
|
|
262
|
+
label: clean(row?.displayName) || id,
|
|
263
|
+
reasoning_efforts: reasoningEfforts.length ? reasoningEfforts : ['auto'],
|
|
264
|
+
source_url: 'https://api.cursor.com/v1/models',
|
|
265
|
+
capabilities: { cursor_cloud_agent: true, agent_workflow: true },
|
|
266
|
+
metadata: {
|
|
267
|
+
description: clean(row?.description) || null,
|
|
268
|
+
aliases: Array.isArray(row?.aliases) ? row.aliases : [],
|
|
269
|
+
parameters: params,
|
|
270
|
+
variants: Array.isArray(row?.variants) ? row.variants : [],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
})
|
|
274
|
+
.filter(Boolean);
|
|
275
|
+
return { attempted: true, ok: true, models, error: null };
|
|
276
|
+
} catch (error) { return failure(error); }
|
|
277
|
+
}
|
|
278
|
+
|
|
245
279
|
function cloudflareTaskName(task) {
|
|
246
280
|
if (typeof task === 'string') return clean(task);
|
|
247
281
|
if (task && typeof task === 'object') return clean(task.name || task.id);
|
|
@@ -286,6 +320,7 @@ export async function discoverProviderModels(provider, credential, options = {})
|
|
|
286
320
|
case 'gemini': return discoverGeminiModels(credential?.value, fetchImpl);
|
|
287
321
|
case 'grok':
|
|
288
322
|
case 'xai': return discoverXaiModels(credential?.value, fetchImpl);
|
|
323
|
+
case 'cursor': return discoverCursorModels(credential?.value, fetchImpl);
|
|
289
324
|
case 'cloudflare': return discoverCloudflareModels(credential?.value, credential?.account_id, fetchImpl);
|
|
290
325
|
default: return failure(`unsupported provider: ${provider}`, false);
|
|
291
326
|
}
|
package/src/ui/cli/help.js
CHANGED
|
@@ -12,7 +12,8 @@ const HELP_TOPICS = Object.freeze([
|
|
|
12
12
|
['agentsam resume [session]', 'Resume a saved Agent Sam session'],
|
|
13
13
|
['agentsam whoami', 'Show authenticated account and credential status'],
|
|
14
14
|
['agentsam models', 'Probe account-visible hosted/local models'],
|
|
15
|
-
['agentsam
|
|
15
|
+
['agentsam providers', 'Configure and verify machine provider credentials'],
|
|
16
|
+
['agentsam env init <provider>', 'Low-level provider profile compatibility command'],
|
|
16
17
|
],
|
|
17
18
|
},
|
|
18
19
|
{
|
|
@@ -48,6 +49,7 @@ const HELP_TOPICS = Object.freeze([
|
|
|
48
49
|
rows: [
|
|
49
50
|
['/', 'Open the keyboard command picker'],
|
|
50
51
|
['/model', 'Choose model, reasoning, and processing tier'],
|
|
52
|
+
['/providers', 'Configure and verify machine provider credentials'],
|
|
51
53
|
['/context', 'Show live context economics'],
|
|
52
54
|
['/usage', 'Show token/cost/session receipt'],
|
|
53
55
|
['/settings', 'Change runtime, terminal, and model policy'],
|
|
@@ -3,7 +3,15 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
accountSessionPath,
|
|
8
|
+
clearAccountSession,
|
|
9
|
+
readAccountSession,
|
|
10
|
+
resolveAccountApiKey,
|
|
11
|
+
resolveAccountAuth,
|
|
12
|
+
resolveBrowserSessionCredential,
|
|
13
|
+
saveAccountSession,
|
|
14
|
+
} from '../src/lib/account-session.js';
|
|
7
15
|
|
|
8
16
|
function tempHome(t) {
|
|
9
17
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-account-'));
|
|
@@ -11,26 +19,55 @@ function tempHome(t) {
|
|
|
11
19
|
return home;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
test('browser
|
|
22
|
+
test('opaque browser login persists in machine-local storage and resolves independently from API keys', t => {
|
|
15
23
|
const home = tempHome(t);
|
|
16
|
-
const saved = saveAccountSession({
|
|
24
|
+
const saved = saveAccountSession({
|
|
25
|
+
access_token: 'browser_session_machine_test',
|
|
26
|
+
user_id: 'au_test',
|
|
27
|
+
account_id: 'acct_test',
|
|
28
|
+
email: 'dev@example.test',
|
|
29
|
+
}, { home });
|
|
17
30
|
assert.equal(saved.user_id, 'au_test');
|
|
31
|
+
|
|
18
32
|
const filename = accountSessionPath({ home });
|
|
19
33
|
assert.equal(fs.existsSync(filename), true);
|
|
20
34
|
if (process.platform !== 'win32') assert.equal(fs.statSync(filename).mode & 0o077, 0);
|
|
21
35
|
|
|
22
36
|
const loaded = readAccountSession({ home });
|
|
23
|
-
assert.equal(loaded.
|
|
24
|
-
const
|
|
25
|
-
assert.equal(
|
|
26
|
-
assert.equal(
|
|
37
|
+
assert.equal(loaded.access_token, 'browser_session_machine_test');
|
|
38
|
+
const browser = resolveBrowserSessionCredential({ env: {}, home });
|
|
39
|
+
assert.equal(browser.source, 'agentsam_browser_oauth');
|
|
40
|
+
assert.equal(browser.kind, 'browser_oauth');
|
|
41
|
+
assert.equal(browser.value, 'browser_session_machine_test');
|
|
42
|
+
assert.equal(resolveAccountApiKey({ env: {}, home }).value, '');
|
|
43
|
+
assert.equal(resolveAccountAuth({ env: {}, home }).value, 'browser_session_machine_test');
|
|
44
|
+
|
|
27
45
|
assert.equal(clearAccountSession({ home }), true);
|
|
28
46
|
assert.equal(readAccountSession({ home }), null);
|
|
29
47
|
});
|
|
30
48
|
|
|
31
|
-
test('explicit/environment
|
|
49
|
+
test('explicit/environment aak_ API key outranks browser login while legacy SDK env names are ignored', t => {
|
|
50
|
+
const home = tempHome(t);
|
|
51
|
+
saveAccountSession({ access_token: 'browser_session_disk' }, { home });
|
|
52
|
+
|
|
53
|
+
assert.equal(
|
|
54
|
+
resolveAccountAuth({ env: { AGENTSAM_API_KEY: 'aak_env_test' }, home }).value,
|
|
55
|
+
'aak_env_test',
|
|
56
|
+
);
|
|
57
|
+
assert.equal(
|
|
58
|
+
resolveAccountAuth({ env: { AGENTSAM_API_KEY: 'aak_env_test' }, explicit: 'aak_explicit_test', home }).value,
|
|
59
|
+
'aak_explicit_test',
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
resolveAccountAuth({ env: { AGENTSAM_SDK_KEY: 'legacy_ignored' }, home }).value,
|
|
63
|
+
'browser_session_disk',
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('browser session storage refuses reusable aak_ credentials', t => {
|
|
32
68
|
const home = tempHome(t);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => saveAccountSession({ access_token: 'aak_should_not_be_browser_session' }, { home }),
|
|
71
|
+
/account_browser_oauth_session_required/,
|
|
72
|
+
);
|
|
36
73
|
});
|
|
@@ -13,6 +13,10 @@ function packageJson(...parts) {
|
|
|
13
13
|
return JSON.parse(fs.readFileSync(at(...parts, 'package.json'), 'utf8'));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function read(...parts) {
|
|
17
|
+
return fs.readFileSync(at(...parts), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
test('product apps are self-contained npm workspace roots with one lockfile', () => {
|
|
17
21
|
for (const app of productApps) {
|
|
18
22
|
const base = ['apps', app];
|
|
@@ -54,3 +58,42 @@ test('Local Studio retired donor-era duplicate deployment configs', () => {
|
|
|
54
58
|
assert.equal(exists('apps', 'local-studio', 'backend', file), false, `retire ${file}`);
|
|
55
59
|
}
|
|
56
60
|
});
|
|
61
|
+
|
|
62
|
+
test('CAD creator app package is curated for third-party installation', () => {
|
|
63
|
+
const pkg = packageJson('apps', 'cad-creator');
|
|
64
|
+
const manifest = JSON.parse(read('apps', 'cad-creator', 'agentsam.app.json'));
|
|
65
|
+
|
|
66
|
+
assert.equal(pkg.name, '@inneranimalmedia/agentsam-sdk-cad-creator');
|
|
67
|
+
assert.notEqual(pkg.private, true);
|
|
68
|
+
assert.equal(pkg.bin['agentsam-cad-creator'], 'bin/agentsam-cad-creator.mjs');
|
|
69
|
+
assert.equal(pkg.publishConfig.access, 'public');
|
|
70
|
+
assert.ok(pkg.files.includes('frontend/dist/'));
|
|
71
|
+
assert.ok(pkg.files.includes('backend/dist/'));
|
|
72
|
+
assert.ok(pkg.files.includes('backend/worker/'));
|
|
73
|
+
assert.equal(pkg.files.some((entry) => entry.startsWith('reference')), false);
|
|
74
|
+
assert.equal(pkg.files.some((entry) => entry.includes('.wrangler/state')), false);
|
|
75
|
+
|
|
76
|
+
assert.equal(manifest.schema, 'agentsam.app.v1');
|
|
77
|
+
assert.equal(manifest.id, 'cad-creator');
|
|
78
|
+
assert.equal(manifest.package, pkg.name);
|
|
79
|
+
assert.equal(manifest.runtime.local_preview, 'ready');
|
|
80
|
+
assert.equal(manifest.runtime.source_scaffold, 'ready');
|
|
81
|
+
assert.equal(manifest.runtime.cloudflare, 'scaffold');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('CAD creator keeps the robotics runtime lazy and the perception key server-side', () => {
|
|
85
|
+
const app = read('apps', 'cad-creator', 'frontend', 'src', 'App.tsx');
|
|
86
|
+
const lazy = read('apps', 'cad-creator', 'frontend', 'src', 'workspaces', 'robotics', 'lazy.tsx');
|
|
87
|
+
const httpProvider = read('apps', 'cad-creator', 'frontend', 'src', 'lib', 'robotics', 'perception', 'http-provider.ts');
|
|
88
|
+
const perception = read('apps', 'cad-creator', 'backend', 'src', 'robotics', 'perception.ts');
|
|
89
|
+
const worker = read('apps', 'cad-creator', 'backend', 'worker', 'index.js');
|
|
90
|
+
|
|
91
|
+
assert.doesNotMatch(app, /from ['\"]\.\/workspaces\/robotics\/RoboticsWorkspace['\"]/);
|
|
92
|
+
assert.doesNotMatch(app, /MujocoSimulationProvider/);
|
|
93
|
+
assert.match(app, /LazyRoboticsWorkspace/);
|
|
94
|
+
assert.match(lazy, /import\(['\"]\.\/RoboticsWorkspace['\"]\)/);
|
|
95
|
+
assert.match(httpProvider, /\/api\/robotics\/perception\/detect/);
|
|
96
|
+
assert.doesNotMatch(httpProvider, /GEMINI_API_KEY|GOOGLE_API_KEY/);
|
|
97
|
+
assert.match(perception, /env\.GEMINI_API_KEY/);
|
|
98
|
+
assert.match(worker, /\/api\/robotics\/perception\/detect/);
|
|
99
|
+
});
|
|
@@ -35,9 +35,13 @@ test('agentsam-sdk Worker uses the backend Worker entry, custom domain only, and
|
|
|
35
35
|
assert.match(wrangler, /"service"\s*:\s*"execos"/);
|
|
36
36
|
assert.match(wrangler, /"binding"\s*:\s*"PTY_SERVICE"/);
|
|
37
37
|
assert.match(wrangler, /"service_id"\s*:\s*"019db639-7c70-7071-8ef3-32ec0392a9ff"/);
|
|
38
|
-
assert.match(wrangler, /"
|
|
38
|
+
assert.match(wrangler, /"IAM_OAUTH_ISSUER"\s*:\s*"https:\/\/inneranimalmedia\.com"/);
|
|
39
|
+
assert.match(wrangler, /"IAM_CLIENT_ID"\s*:\s*"iam_agentsam_sdk_web"/);
|
|
40
|
+
assert.doesNotMatch(wrangler, /"IAM_ORIGIN"\s*:/);
|
|
39
41
|
assert.doesNotMatch(wrangler, /AGENTSAM_WORKER_ROLE/);
|
|
40
42
|
assert.doesNotMatch(wrangler, /"OLLAMA_BASE_URL"\s*:/);
|
|
43
|
+
assert.doesNotMatch(wrangler, /"OLLAMA_MODEL"\s*:/);
|
|
44
|
+
assert.doesNotMatch(wrangler, /"OLLAMA_EMBED_MODEL"\s*:/);
|
|
41
45
|
assert.doesNotMatch(wrangler, /workers\.dev/);
|
|
42
46
|
});
|
|
43
47
|
|
package/test/smoke.mjs
CHANGED
|
@@ -56,6 +56,7 @@ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/deploy'));
|
|
|
56
56
|
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/db'));
|
|
57
57
|
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/agent'));
|
|
58
58
|
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/model'));
|
|
59
|
+
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/providers'));
|
|
59
60
|
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/reasoning'));
|
|
60
61
|
assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/fast'));
|
|
61
62
|
assert.deepEqual(
|
|
@@ -65,7 +66,7 @@ assert.deepEqual(
|
|
|
65
66
|
);
|
|
66
67
|
|
|
67
68
|
printContextSummary({
|
|
68
|
-
iam: { ready: true, source: '
|
|
69
|
+
iam: { ready: true, source: 'api_key', detail: 'AGENTSAM_API_KEY' },
|
|
69
70
|
gcp: { source: 'vm-metadata', email: 'execos@project.iam.gserviceaccount.com' },
|
|
70
71
|
gcp_vm: true,
|
|
71
72
|
github: { source: 'gh-cli', account: 'user@example.com' },
|
|
@@ -14,9 +14,9 @@ function tempHome(t) {
|
|
|
14
14
|
return home;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
test('whoami validates persisted IAM identity while never returning
|
|
17
|
+
test('whoami validates persisted IAM browser identity while never returning account or provider secrets', async t => {
|
|
18
18
|
const home = tempHome(t);
|
|
19
|
-
saveAccountSession({ access_token: '
|
|
19
|
+
saveAccountSession({ access_token: 'browser_session_do_not_print', user_id: 'au_local' }, { home });
|
|
20
20
|
const envDir = path.join(home, '.agentsam', 'env.d');
|
|
21
21
|
fs.mkdirSync(envDir, { recursive: true });
|
|
22
22
|
const openaiFile = path.join(envDir, 'openai.env');
|
|
@@ -26,16 +26,17 @@ test('whoami validates persisted IAM identity while never returning the SDK or p
|
|
|
26
26
|
const status = await collectWhoami({
|
|
27
27
|
env: {}, home,
|
|
28
28
|
contextLoader: async token => {
|
|
29
|
-
assert.equal(token, '
|
|
29
|
+
assert.equal(token, 'browser_session_do_not_print');
|
|
30
30
|
return { user_id: 'au_server', account_id: 'acct_server', email: 'dev@example.test', cloudflare: { ok: true }, byok: { openai: { configured: true, masked: 'secret' } } };
|
|
31
31
|
},
|
|
32
32
|
});
|
|
33
33
|
assert.equal(status.authenticated, true);
|
|
34
34
|
assert.equal(status.identity.account_id, 'acct_server');
|
|
35
|
-
assert.equal(status.
|
|
35
|
+
assert.equal(status.active_auth.source, 'agentsam_browser_oauth');
|
|
36
|
+
assert.equal(status.active_auth.kind, 'browser_oauth');
|
|
36
37
|
assert.equal(status.provider_credentials.find(row => row.provider === 'openai').configured, true);
|
|
37
38
|
const serialized = JSON.stringify(status);
|
|
38
|
-
assert.doesNotMatch(serialized, /
|
|
39
|
+
assert.doesNotMatch(serialized, /browser_session_do_not_print|sk-never-print-this|masked/);
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
test('resume restores saved cwd and session through the canonical shell runtime', async t => {
|
package/src/lib/prompt-byok.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional BYOK key paste during SDK init → CORE user_api_keys.
|
|
3
|
-
*/
|
|
4
|
-
import { getJson, postJson } from './core-client.js';
|
|
5
|
-
|
|
6
|
-
const PROVIDERS = [
|
|
7
|
-
{ id: 'openai', label: 'OpenAI' },
|
|
8
|
-
{ id: 'anthropic', label: 'Anthropic' },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {string} token SDK bearer
|
|
13
|
-
* @param {{ ask: (q: string) => Promise<string> } | null} prompt
|
|
14
|
-
*/
|
|
15
|
-
export async function promptOptionalByokKeys(token, prompt) {
|
|
16
|
-
if (!token || !prompt) return;
|
|
17
|
-
|
|
18
|
-
let ctx;
|
|
19
|
-
try {
|
|
20
|
-
ctx = await getJson('/api/sdk/context', token);
|
|
21
|
-
} catch {
|
|
22
|
-
console.log('\n ⚠ Could not load BYOK status — skip key paste or add keys in Dashboard → Settings → Keys\n');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const byok = ctx?.byok || {};
|
|
27
|
-
console.log('\n Provider keys (BYOK — optional, stored in YOUR IAM account):\n');
|
|
28
|
-
|
|
29
|
-
for (const p of PROVIDERS) {
|
|
30
|
-
const slot = byok[p.id];
|
|
31
|
-
if (slot?.configured) {
|
|
32
|
-
console.log(` ${p.label.padEnd(12)} ✓ connected ${slot.masked || ''}`);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const ans = await prompt.ask(` ${p.label} API key (Enter to skip): `);
|
|
37
|
-
const key = ans.trim();
|
|
38
|
-
if (!key) continue;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await postJson(
|
|
42
|
-
'/api/sdk/keys',
|
|
43
|
-
{
|
|
44
|
-
provider: p.id,
|
|
45
|
-
api_key: key,
|
|
46
|
-
label: `${p.label} (SDK init)`,
|
|
47
|
-
validate: true,
|
|
48
|
-
},
|
|
49
|
-
token,
|
|
50
|
-
);
|
|
51
|
-
console.log(` ${p.label.padEnd(12)} ✓ saved to IAM BYOK`);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.log(` ${p.label.padEnd(12)} ✗ ${e?.message || 'save failed'}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
console.log('');
|
|
57
|
-
}
|