@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.
@@ -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
- gemini: Object.freeze({ env: 'GEMINI_API_KEY', files: ['gemini.env'] }),
8
- anthropic: Object.freeze({ env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'] }),
9
- grok: Object.freeze({ env: 'XAI_API_KEY', files: ['grok.env', 'xai.env'] }),
10
- cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'], accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'] }),
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
- fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
124
+ const supported = Object.keys(PROVIDER_CREDENTIALS).join('|');
72
125
  const source = `# AgentSam provider environment loader. Source this file; do not execute it.
73
- _agentsam_profile=\"\${1:-}\"
74
- case \"\$_agentsam_profile\" in
75
- openai|anthropic|gemini|grok|cloudflare) ;;
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
- fs.writeFileSync(filename, source, { mode: 0o700 });
93
- if (process.platform !== 'win32') fs.chmodSync(filename, 0o700);
151
+ atomicWrite(filename, source, 0o700);
94
152
  return filename;
95
153
  }
96
154
 
97
- function profileTemplate(provider, options = {}) {
98
- if (provider === 'cloudflare') {
99
- const accountId = normalizeCloudflareAccountId(options.accountId);
100
- return `# AgentSam Cloudflare profile\n# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.\nexport ACCOUNT_ID=\"${accountId}\"\nexport CLOUDFLARE_API_TOKEN=\"\"\n`;
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
- const spec = PROVIDER_CREDENTIALS[provider];
103
- if (!spec) throw new Error(`unsupported_provider:${provider}`);
104
- return `# AgentSam ${provider} provider profile\nexport ${spec.env}=\"\"\n`;
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:${clean(provider).toLowerCase()}`);
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
- fs.writeFileSync(filename, profileTemplate(spec.provider, options), { mode: 0o600 });
182
+ atomicWrite(filename, profileSource(spec.provider, '', options), 0o600);
117
183
  created = true;
118
- } else if (spec.provider === 'cloudflare' && clean(options.accountId)) {
119
- const source = fs.readFileSync(filename, 'utf8');
120
- const current = firstEnvValue(source, spec.accountEnv || []);
121
- if (!current) {
122
- const accountId = normalizeCloudflareAccountId(options.accountId);
123
- const line = `export ACCOUNT_ID=\"${accountId}\"`;
124
- const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
125
- ? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
126
- : `${line}\n${source}`;
127
- fs.writeFileSync(filename, next, { mode: 0o600 });
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({ provider: spec.provider, file: filename, loader, created, source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}` });
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 providerCredentialSpec(provider) {
136
- const id = clean(provider).toLowerCase();
137
- const spec = PROVIDER_CREDENTIALS[id];
138
- return spec ? Object.freeze({ provider: id, ...spec }) : null;
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: clean(provider).toLowerCase(), configured: false, source: null, error: 'unsupported_provider', value: '' });
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) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv, account_id: accountIdFromEnv || null });
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
- if (value) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
160
- return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: 'credential_variable_missing', value: '' });
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' },
@@ -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
  }
@@ -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 env init <provider>', 'Create a secure local provider profile'],
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 { accountSessionPath, clearAccountSession, readAccountSession, resolveAccountSdkKey, saveAccountSession } from '../src/lib/account-session.js';
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-auth SDK bearer persists in machine-local storage, not project state', t => {
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({ access_token: 'sdk_machine_session', user_id: 'au_test', account_id: 'acct_test', email: 'dev@example.test' }, { home });
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.sdk_key, 'sdk_machine_session');
24
- const resolved = resolveAccountSdkKey({ env: {}, home });
25
- assert.equal(resolved.source, 'agentsam_account_session');
26
- assert.equal(resolved.value, 'sdk_machine_session');
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 SDK bearer remains higher authority than local session fallback', t => {
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
- saveAccountSession({ access_token: 'sdk_disk' }, { home });
34
- assert.equal(resolveAccountSdkKey({ env: { AGENTSAM_SDK_KEY: 'sdk_env' }, home }).value, 'sdk_env');
35
- assert.equal(resolveAccountSdkKey({ env: { AGENTSAM_SDK_KEY: 'sdk_env' }, explicit: 'sdk_explicit', home }).value, 'sdk_explicit');
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, /"IAM_ORIGIN"\s*:\s*"https:\/\/inneranimalmedia\.com"/);
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: 'sdk-key', detail: 'AGENTSAM_SDK_KEY' },
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 the SDK or provider secret', async t => {
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: 'sdk_do_not_print', user_id: 'au_local' }, { home });
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, 'sdk_do_not_print');
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.sdk_credential.source, 'agentsam_account_session');
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, /sdk_do_not_print|sk-never-print-this|masked/);
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 => {
@@ -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
- }