@inneranimalmedia/agentsam-sdk 2.6.0 → 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/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +7 -7
- package/docs/SDK_WORKER.md +4 -3
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +28 -7
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- 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/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/src/agent/responses-runner.js +63 -35
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +36 -29
- package/src/commands/account-auth.js +1 -1
- package/src/commands/context-economics.js +17 -2
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/deploy.js +2 -2
- package/src/commands/env.js +90 -0
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +123 -65
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +53 -26
- package/src/commands/providers.js +308 -0
- package/src/commands/shell.js +240 -48
- package/src/commands/tunnel.js +8 -8
- package/src/commands/whoami.js +42 -21
- package/src/errors/contract.js +236 -0
- package/src/errors/diagnostic.js +1 -0
- package/src/errors/index.js +14 -0
- package/src/index.js +13 -1
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/account-session.js +94 -25
- package/src/lib/auth.js +314 -51
- package/src/lib/cli-preferences.js +31 -4
- package/src/lib/core-client.js +75 -30
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/detect-context.js +14 -16
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +23 -2
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +230 -14
- package/src/lib/slash-commands.js +5 -3
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/discovery.js +327 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +5 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +226 -75
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/events.js +4 -1
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +194 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/account-session.test.mjs +48 -11
- package/test/apps-scaffold-contract.test.mjs +43 -0
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/error-diagnostics.test.mjs +57 -1
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +7 -1
- package/test/models.test.mjs +101 -4
- package/test/ollama.test.mjs +21 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +45 -1
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +3 -1
- package/test/sdk-worker-contract.test.mjs +5 -1
- package/test/shell.test.mjs +50 -8
- package/test/smoke.mjs +2 -1
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- 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
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { isApiKey, resolveApiKey } from '../../packages/identity/src/contracts/auth-config.js';
|
|
5
5
|
|
|
6
|
-
export const ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-
|
|
6
|
+
export const ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-v3';
|
|
7
|
+
const LEGACY_ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-v2';
|
|
7
8
|
|
|
8
9
|
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
9
10
|
function homeDirectory(options = {}) {
|
|
10
11
|
return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
11
12
|
}
|
|
13
|
+
function isoOrNull(value) {
|
|
14
|
+
const raw = clean(value);
|
|
15
|
+
if (!raw) return null;
|
|
16
|
+
const ms = Date.parse(raw);
|
|
17
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
18
|
+
}
|
|
19
|
+
function expiresAtFrom(session = {}, nowMs = Date.now()) {
|
|
20
|
+
const explicit = isoOrNull(session.expires_at);
|
|
21
|
+
if (explicit) return explicit;
|
|
22
|
+
const expiresIn = Number(session.expires_in);
|
|
23
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) return null;
|
|
24
|
+
return new Date(nowMs + (expiresIn * 1000)).toISOString();
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
export function accountSessionPath(options = {}) {
|
|
14
28
|
return path.join(homeDirectory(options), '.agentsam', 'auth', 'session.json');
|
|
@@ -22,17 +36,24 @@ export function readAccountSession(options = {}) {
|
|
|
22
36
|
if (!stat.isFile()) return null;
|
|
23
37
|
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) return null;
|
|
24
38
|
const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
25
|
-
if (parsed?.schema_version
|
|
26
|
-
const
|
|
27
|
-
|
|
39
|
+
if (![ACCOUNT_SESSION_SCHEMA, LEGACY_ACCOUNT_SESSION_SCHEMA].includes(parsed?.schema_version)) return null;
|
|
40
|
+
const accessToken = clean(parsed.access_token);
|
|
41
|
+
const refreshToken = clean(parsed.refresh_token);
|
|
42
|
+
if (!accessToken || isApiKey(accessToken) || (refreshToken && isApiKey(refreshToken))) return null;
|
|
28
43
|
return {
|
|
29
44
|
schema_version: ACCOUNT_SESSION_SCHEMA,
|
|
30
|
-
|
|
45
|
+
access_token: accessToken,
|
|
46
|
+
refresh_token: refreshToken || null,
|
|
47
|
+
token_type: clean(parsed.token_type) || 'Bearer',
|
|
48
|
+
scope: clean(parsed.scope) || null,
|
|
49
|
+
expires_at: isoOrNull(parsed.expires_at),
|
|
50
|
+
client_id: clean(parsed.client_id) || null,
|
|
31
51
|
user_id: clean(parsed.user_id) || null,
|
|
32
52
|
account_id: clean(parsed.account_id) || null,
|
|
33
53
|
email: clean(parsed.email) || null,
|
|
34
|
-
created_at:
|
|
35
|
-
updated_at:
|
|
54
|
+
created_at: isoOrNull(parsed.created_at),
|
|
55
|
+
updated_at: isoOrNull(parsed.updated_at),
|
|
56
|
+
migrated_from: parsed.schema_version === LEGACY_ACCOUNT_SESSION_SCHEMA ? LEGACY_ACCOUNT_SESSION_SCHEMA : null,
|
|
36
57
|
};
|
|
37
58
|
} catch {
|
|
38
59
|
return null;
|
|
@@ -40,25 +61,38 @@ export function readAccountSession(options = {}) {
|
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
export function saveAccountSession(session = {}, options = {}) {
|
|
43
|
-
const
|
|
44
|
-
|
|
64
|
+
const accessToken = clean(session.access_token);
|
|
65
|
+
const suppliedRefresh = clean(session.refresh_token);
|
|
66
|
+
if (!accessToken || isApiKey(accessToken) || (suppliedRefresh && isApiKey(suppliedRefresh))) {
|
|
67
|
+
throw new Error('account_browser_oauth_session_required');
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
const filename = accountSessionPath(options);
|
|
46
71
|
const dir = path.dirname(filename);
|
|
47
72
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
48
73
|
if (process.platform !== 'win32') {
|
|
49
74
|
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
50
75
|
}
|
|
76
|
+
|
|
51
77
|
const previous = readAccountSession(options);
|
|
52
|
-
const
|
|
78
|
+
const nowMs = Number.isFinite(Number(options.nowMs)) ? Number(options.nowMs) : Date.now();
|
|
79
|
+
const now = new Date(nowMs).toISOString();
|
|
80
|
+
const preserveRefreshToken = options.preserveRefreshToken !== false;
|
|
53
81
|
const value = {
|
|
54
82
|
schema_version: ACCOUNT_SESSION_SCHEMA,
|
|
55
|
-
|
|
83
|
+
access_token: accessToken,
|
|
84
|
+
refresh_token: suppliedRefresh || (preserveRefreshToken ? previous?.refresh_token : null) || null,
|
|
85
|
+
token_type: clean(session.token_type) || previous?.token_type || 'Bearer',
|
|
86
|
+
scope: clean(session.scope) || previous?.scope || null,
|
|
87
|
+
expires_at: expiresAtFrom(session, nowMs),
|
|
88
|
+
client_id: clean(session.client_id) || previous?.client_id || null,
|
|
56
89
|
user_id: clean(session.user_id) || previous?.user_id || null,
|
|
57
90
|
account_id: clean(session.account_id) || previous?.account_id || null,
|
|
58
91
|
email: clean(session.email) || previous?.email || null,
|
|
59
92
|
created_at: previous?.created_at || now,
|
|
60
93
|
updated_at: now,
|
|
61
94
|
};
|
|
95
|
+
|
|
62
96
|
const temp = `${filename}.${process.pid}.tmp`;
|
|
63
97
|
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
64
98
|
if (process.platform !== 'win32') {
|
|
@@ -75,24 +109,59 @@ export function clearAccountSession(options = {}) {
|
|
|
75
109
|
return true;
|
|
76
110
|
}
|
|
77
111
|
|
|
78
|
-
export function
|
|
112
|
+
export function isBrowserSessionExpired(session, options = {}) {
|
|
113
|
+
const expiresAt = Date.parse(clean(session?.expires_at));
|
|
114
|
+
if (!Number.isFinite(expiresAt)) return false;
|
|
115
|
+
const nowMs = Number.isFinite(Number(options.nowMs)) ? Number(options.nowMs) : Date.now();
|
|
116
|
+
const skewMs = Number.isFinite(Number(options.skewMs)) ? Number(options.skewMs) : 60_000;
|
|
117
|
+
return expiresAt <= (nowMs + Math.max(0, skewMs));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveAccountApiKey(options = {}) {
|
|
79
121
|
const env = options.env || process.env;
|
|
80
122
|
const explicit = clean(options.explicit);
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
const value = resolveApiKey(env, explicit);
|
|
124
|
+
if (!value) return { value: '', source: null, kind: null };
|
|
125
|
+
if (!isApiKey(value)) return { value: '', source: explicit ? 'explicit' : 'environment', kind: 'api_key', error: 'invalid_api_key_prefix' };
|
|
126
|
+
return { value, source: explicit ? 'explicit' : 'environment', kind: 'api_key' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveBrowserSessionCredential(options = {}) {
|
|
130
|
+
const session = readAccountSession(options);
|
|
131
|
+
if (!session?.access_token) return { value: '', source: null, kind: null, session: null, expired: false };
|
|
132
|
+
const expired = isBrowserSessionExpired(session, options);
|
|
133
|
+
return {
|
|
134
|
+
value: expired ? '' : session.access_token,
|
|
135
|
+
source: 'agentsam_browser_oauth',
|
|
136
|
+
kind: 'browser_oauth',
|
|
137
|
+
session,
|
|
138
|
+
expired,
|
|
139
|
+
error: expired ? 'browser_oauth_session_expired' : null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Synchronous authority snapshot kept for low-level compatibility.
|
|
145
|
+
* Runtime SDK calls should use resolveAccountAuthority() from auth.js so expired
|
|
146
|
+
* OAuth sessions can refresh before a request is sent.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveAccountAuth(options = {}) {
|
|
149
|
+
const apiKey = resolveAccountApiKey(options);
|
|
150
|
+
if (apiKey.value || apiKey.error) return apiKey;
|
|
151
|
+
return resolveBrowserSessionCredential(options);
|
|
87
152
|
}
|
|
88
153
|
|
|
89
154
|
export function describeAccountSession(options = {}) {
|
|
90
|
-
const
|
|
155
|
+
const session = readAccountSession(options);
|
|
91
156
|
return {
|
|
92
|
-
configured: Boolean(
|
|
93
|
-
source:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
157
|
+
configured: Boolean(session?.access_token),
|
|
158
|
+
source: session?.access_token ? 'agentsam_browser_oauth' : null,
|
|
159
|
+
kind: session?.access_token ? 'browser_oauth' : null,
|
|
160
|
+
refreshable: Boolean(session?.refresh_token),
|
|
161
|
+
expires_at: session?.expires_at || null,
|
|
162
|
+
expired: session ? isBrowserSessionExpired(session, options) : false,
|
|
163
|
+
user_id: session?.user_id || null,
|
|
164
|
+
account_id: session?.account_id || null,
|
|
165
|
+
email: session?.email || null,
|
|
97
166
|
};
|
|
98
167
|
}
|
package/src/lib/auth.js
CHANGED
|
@@ -1,71 +1,334 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* RFC 8252 native-app OAuth for AgentSam CLI.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is a public client: authorization code + PKCE over a loopback
|
|
5
|
+
* redirect. Browser OAuth sessions are machine-local and remain separate from
|
|
6
|
+
* reusable AGENTSAM_API_KEY (aak_*) credentials.
|
|
3
7
|
*/
|
|
4
8
|
import http from 'node:http';
|
|
5
|
-
import { randomBytes } from 'node:crypto';
|
|
6
|
-
import {
|
|
9
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
10
|
+
import { resolveIamIssuer } from '../../packages/identity/src/contracts/auth-config.js';
|
|
7
11
|
import { promptToOpenUrl } from './open-url.js';
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isBrowserSessionExpired,
|
|
14
|
+
readAccountSession,
|
|
15
|
+
resolveAccountApiKey,
|
|
16
|
+
saveAccountSession,
|
|
17
|
+
} from './account-session.js';
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
export const AGENTSAM_NATIVE_OAUTH_CLIENT_ID = 'iam_cli_agentsam';
|
|
20
|
+
export const AGENTSAM_NATIVE_OAUTH_SCOPE = 'openid profile email offline_access';
|
|
21
|
+
export const AGENTSAM_OAUTH_CALLBACK_PATH = '/callback';
|
|
22
|
+
|
|
23
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
24
|
+
function base64url(value) {
|
|
25
|
+
return Buffer.from(value).toString('base64')
|
|
26
|
+
.replace(/=/g, '')
|
|
27
|
+
.replace(/\+/g, '-')
|
|
28
|
+
.replace(/\//g, '_');
|
|
29
|
+
}
|
|
30
|
+
function randomUrlSafe(bytes = 32, randomBytesImpl = randomBytes) {
|
|
31
|
+
return base64url(randomBytesImpl(bytes));
|
|
32
|
+
}
|
|
33
|
+
function oauthErrorMessage(body, status) {
|
|
34
|
+
const code = clean(body?.error);
|
|
35
|
+
const description = clean(body?.error_description || body?.message);
|
|
36
|
+
if (code && description) return `${code}: ${description}`;
|
|
37
|
+
return code || description || `OAuth token HTTP ${status}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createPkcePair(options = {}) {
|
|
41
|
+
const verifier = randomUrlSafe(32, options.randomBytesImpl || randomBytes);
|
|
42
|
+
const challenge = base64url(createHash('sha256').update(verifier, 'ascii').digest());
|
|
43
|
+
return Object.freeze({ verifier, challenge, method: 'S256' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildNativeAuthorizationUrl(options = {}) {
|
|
47
|
+
const env = options.env || process.env;
|
|
48
|
+
const issuer = resolveIamIssuer(env, options.issuer || '');
|
|
49
|
+
const clientId = clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
|
|
50
|
+
const redirectUri = clean(options.redirectUri);
|
|
51
|
+
const state = clean(options.state);
|
|
52
|
+
const codeChallenge = clean(options.codeChallenge);
|
|
53
|
+
const scope = clean(options.scope ?? AGENTSAM_NATIVE_OAUTH_SCOPE);
|
|
54
|
+
if (!redirectUri || !state || !codeChallenge) throw new Error('oauth_authorization_parameters_required');
|
|
55
|
+
|
|
56
|
+
const url = new URL('/api/oauth/authorize', `${issuer}/`);
|
|
57
|
+
url.searchParams.set('response_type', 'code');
|
|
58
|
+
url.searchParams.set('client_id', clientId);
|
|
59
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
60
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
61
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
62
|
+
url.searchParams.set('state', state);
|
|
63
|
+
if (scope) url.searchParams.set('scope', scope);
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function oauthTokenRequest(params, options = {}) {
|
|
68
|
+
const env = options.env || process.env;
|
|
69
|
+
const issuer = resolveIamIssuer(env, options.issuer || '');
|
|
70
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
71
|
+
const body = new URLSearchParams();
|
|
72
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
73
|
+
const normalized = clean(value);
|
|
74
|
+
if (normalized) body.set(key, normalized);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const response = await fetchImpl(new URL('/api/oauth/token', `${issuer}/`).toString(), {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
Accept: 'application/json',
|
|
81
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
82
|
+
},
|
|
83
|
+
body: body.toString(),
|
|
84
|
+
signal: options.signal || (typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(15_000) : undefined),
|
|
85
|
+
});
|
|
86
|
+
const data = await response.json().catch(() => ({}));
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const error = new Error(oauthErrorMessage(data, response.status));
|
|
89
|
+
error.status = response.status;
|
|
90
|
+
error.oauth_error = clean(data?.error) || null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (!clean(data?.access_token)) throw new Error('oauth_token_response_missing_access_token');
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function exchangeAuthorizationCode(options = {}) {
|
|
98
|
+
const code = clean(options.code);
|
|
99
|
+
const codeVerifier = clean(options.codeVerifier);
|
|
100
|
+
const redirectUri = clean(options.redirectUri);
|
|
101
|
+
if (!code || !codeVerifier || !redirectUri) throw new Error('oauth_authorization_code_exchange_parameters_required');
|
|
102
|
+
return oauthTokenRequest({
|
|
103
|
+
grant_type: 'authorization_code',
|
|
104
|
+
client_id: clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID,
|
|
105
|
+
redirect_uri: redirectUri,
|
|
106
|
+
code,
|
|
107
|
+
code_verifier: codeVerifier,
|
|
108
|
+
}, options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function refreshAccountSession(options = {}) {
|
|
112
|
+
const session = options.session || readAccountSession(options);
|
|
113
|
+
if (!session?.refresh_token) throw new Error('browser_oauth_refresh_unavailable');
|
|
114
|
+
const clientId = clean(session.client_id) || clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
|
|
115
|
+
const refreshed = await oauthTokenRequest({
|
|
116
|
+
grant_type: 'refresh_token',
|
|
117
|
+
client_id: clientId,
|
|
118
|
+
refresh_token: session.refresh_token,
|
|
119
|
+
}, options);
|
|
120
|
+
|
|
121
|
+
return saveAccountSession({
|
|
122
|
+
...refreshed,
|
|
123
|
+
refresh_token: clean(refreshed.refresh_token) || session.refresh_token,
|
|
124
|
+
client_id: clientId,
|
|
125
|
+
user_id: session.user_id,
|
|
126
|
+
account_id: session.account_id,
|
|
127
|
+
email: session.email,
|
|
128
|
+
}, {
|
|
129
|
+
...options,
|
|
130
|
+
preserveRefreshToken: true,
|
|
131
|
+
});
|
|
12
132
|
}
|
|
13
133
|
|
|
14
134
|
/**
|
|
15
|
-
*
|
|
135
|
+
* Canonical SDK account authority resolution.
|
|
136
|
+
* explicit aak_* -> AGENTSAM_API_KEY -> stored browser OAuth -> OAuth refresh.
|
|
16
137
|
*/
|
|
17
|
-
export async function
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
138
|
+
export async function resolveAccountAuthority(options = {}) {
|
|
139
|
+
const apiKey = resolveAccountApiKey(options);
|
|
140
|
+
if (apiKey.value || apiKey.error) return apiKey;
|
|
21
141
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
142
|
+
let session = readAccountSession(options);
|
|
143
|
+
if (!session?.access_token) return { value: '', source: null, kind: null, session: null };
|
|
144
|
+
|
|
145
|
+
if (isBrowserSessionExpired(session, options)) {
|
|
146
|
+
if (!session.refresh_token) {
|
|
147
|
+
return {
|
|
148
|
+
value: '',
|
|
149
|
+
source: 'agentsam_browser_oauth',
|
|
150
|
+
kind: 'browser_oauth',
|
|
151
|
+
session,
|
|
152
|
+
error: 'browser_oauth_session_expired',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const refreshImpl = options.refreshImpl || refreshAccountSession;
|
|
157
|
+
session = await refreshImpl({ ...options, session });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
value: '',
|
|
161
|
+
source: 'agentsam_browser_oauth',
|
|
162
|
+
kind: 'browser_oauth',
|
|
163
|
+
session,
|
|
164
|
+
error: `browser_oauth_refresh_failed: ${error?.message || String(error)}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
value: session.access_token,
|
|
171
|
+
source: 'agentsam_browser_oauth',
|
|
172
|
+
kind: 'browser_oauth',
|
|
173
|
+
session,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function createLoopbackCallbackListener(options = {}) {
|
|
178
|
+
const host = clean(options.host) || '127.0.0.1';
|
|
179
|
+
const callbackPath = clean(options.callbackPath) || AGENTSAM_OAUTH_CALLBACK_PATH;
|
|
180
|
+
const expectedState = clean(options.state);
|
|
181
|
+
if (!expectedState) throw new Error('oauth_state_required');
|
|
182
|
+
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 180_000;
|
|
183
|
+
const createServerImpl = options.createServerImpl || http.createServer;
|
|
184
|
+
|
|
185
|
+
let settle;
|
|
186
|
+
let settled = false;
|
|
187
|
+
let timer = null;
|
|
188
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
189
|
+
settle = (error, result) => {
|
|
190
|
+
if (settled) return;
|
|
191
|
+
settled = true;
|
|
192
|
+
if (timer) clearTimeout(timer);
|
|
193
|
+
if (error) reject(error);
|
|
194
|
+
else resolve(result);
|
|
195
|
+
};
|
|
25
196
|
});
|
|
26
197
|
|
|
27
|
-
|
|
198
|
+
const server = createServerImpl((req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const requestUrl = new URL(req.url || '/', `http://${host}`);
|
|
201
|
+
if (requestUrl.pathname !== callbackPath) {
|
|
202
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
203
|
+
res.end('Not found');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
28
206
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const gotState = u.searchParams.get('state');
|
|
40
|
-
if (!code || gotState !== state) {
|
|
41
|
-
res.writeHead(400);
|
|
42
|
-
res.end('Invalid callback');
|
|
43
|
-
reject(new Error('auth callback invalid'));
|
|
44
|
-
server.close();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
48
|
-
res.end('<html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>Authentication complete. You can close this tab and return to your terminal.</p></body></html>');
|
|
49
|
-
resolve(code);
|
|
50
|
-
server.close();
|
|
51
|
-
} catch (e) {
|
|
52
|
-
reject(e);
|
|
53
|
-
server.close();
|
|
207
|
+
const returnedState = clean(requestUrl.searchParams.get('state'));
|
|
208
|
+
const oauthError = clean(requestUrl.searchParams.get('error'));
|
|
209
|
+
const oauthDescription = clean(requestUrl.searchParams.get('error_description'));
|
|
210
|
+
const code = clean(requestUrl.searchParams.get('code'));
|
|
211
|
+
|
|
212
|
+
if (!returnedState || returnedState !== expectedState) {
|
|
213
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
214
|
+
res.end('Invalid OAuth state. Return to the terminal and retry.');
|
|
215
|
+
settle(new Error('oauth_state_mismatch'));
|
|
216
|
+
return;
|
|
54
217
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
218
|
+
if (oauthError) {
|
|
219
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
220
|
+
res.end('Authorization was not completed. Return to the terminal.');
|
|
221
|
+
settle(new Error(oauthDescription ? `${oauthError}: ${oauthDescription}` : oauthError));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!code) {
|
|
225
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
226
|
+
res.end('Authorization code missing. Return to the terminal and retry.');
|
|
227
|
+
settle(new Error('oauth_authorization_code_missing'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
232
|
+
res.end('<!doctype html><html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>Authentication complete. You can close this tab and return to your terminal.</p></body></html>');
|
|
233
|
+
settle(null, { code, state: returnedState });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
try {
|
|
236
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
237
|
+
res.end('OAuth callback failed. Return to the terminal.');
|
|
238
|
+
} catch { /* response may already be closed */ }
|
|
239
|
+
settle(error);
|
|
240
|
+
}
|
|
58
241
|
});
|
|
59
242
|
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
const onError = (error) => {
|
|
245
|
+
server.off('listening', onListening);
|
|
246
|
+
reject(error);
|
|
247
|
+
};
|
|
248
|
+
const onListening = () => {
|
|
249
|
+
server.off('error', onError);
|
|
250
|
+
resolve();
|
|
251
|
+
};
|
|
252
|
+
server.once('error', onError);
|
|
253
|
+
server.once('listening', onListening);
|
|
254
|
+
server.listen({ host, port: Number(options.port) || 0, exclusive: true });
|
|
63
255
|
});
|
|
64
256
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
257
|
+
const address = server.address();
|
|
258
|
+
if (!address || typeof address === 'string') {
|
|
259
|
+
server.close();
|
|
260
|
+
throw new Error('oauth_loopback_listener_address_unavailable');
|
|
261
|
+
}
|
|
262
|
+
const redirectUri = `http://${host}:${address.port}${callbackPath}`;
|
|
263
|
+
timer = setTimeout(() => settle(new Error('oauth_callback_timeout')), Math.max(1, timeoutMs));
|
|
264
|
+
timer.unref?.();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
redirectUri,
|
|
268
|
+
waitForCallback: () => callbackPromise,
|
|
269
|
+
close: () => new Promise((resolve) => {
|
|
270
|
+
if (!server.listening) return resolve();
|
|
271
|
+
server.close(() => resolve());
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function authenticateViaBrowser(options = {}) {
|
|
277
|
+
const env = options.env || process.env;
|
|
278
|
+
const clientId = clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
|
|
279
|
+
const state = randomUrlSafe(24, options.randomBytesImpl || randomBytes);
|
|
280
|
+
const pkce = createPkcePair({ randomBytesImpl: options.randomBytesImpl });
|
|
281
|
+
const listener = await createLoopbackCallbackListener({
|
|
282
|
+
state,
|
|
283
|
+
host: options.host,
|
|
284
|
+
port: options.port,
|
|
285
|
+
callbackPath: options.callbackPath,
|
|
286
|
+
timeoutMs: options.timeoutMs,
|
|
287
|
+
createServerImpl: options.createServerImpl,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const authorizationUrl = buildNativeAuthorizationUrl({
|
|
292
|
+
env,
|
|
293
|
+
issuer: options.issuer,
|
|
294
|
+
clientId,
|
|
295
|
+
redirectUri: listener.redirectUri,
|
|
296
|
+
state,
|
|
297
|
+
codeChallenge: pkce.challenge,
|
|
298
|
+
scope: options.scope,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const promptImpl = options.promptToOpenUrlImpl || promptToOpenUrl;
|
|
302
|
+
await promptImpl(authorizationUrl, {
|
|
303
|
+
heading: 'Authenticate your InnerAnimalMedia account at:',
|
|
304
|
+
prompt: 'Press ENTER to open InnerAnimalMedia sign-in in your browser.',
|
|
305
|
+
input: options.input,
|
|
306
|
+
output: options.output,
|
|
307
|
+
openImpl: options.openImpl,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const callback = await listener.waitForCallback();
|
|
311
|
+
const tokenSet = await exchangeAuthorizationCode({
|
|
312
|
+
env,
|
|
313
|
+
issuer: options.issuer,
|
|
314
|
+
clientId,
|
|
315
|
+
redirectUri: listener.redirectUri,
|
|
316
|
+
code: callback.code,
|
|
317
|
+
codeVerifier: pkce.verifier,
|
|
318
|
+
fetchImpl: options.fetchImpl,
|
|
319
|
+
signal: options.signal,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return saveAccountSession({
|
|
323
|
+
...tokenSet,
|
|
324
|
+
client_id: clientId,
|
|
325
|
+
}, {
|
|
326
|
+
home: options.home,
|
|
327
|
+
env,
|
|
328
|
+
nowMs: options.nowMs,
|
|
329
|
+
preserveRefreshToken: false,
|
|
330
|
+
});
|
|
331
|
+
} finally {
|
|
332
|
+
await listener.close();
|
|
69
333
|
}
|
|
70
|
-
return session;
|
|
71
334
|
}
|
|
@@ -3,8 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { getProjectName, tryReadProjectConfig } from './project-config.js';
|
|
5
5
|
|
|
6
|
-
export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-
|
|
7
|
-
export const
|
|
6
|
+
export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v3';
|
|
7
|
+
export const LEGACY_CLI_PREFERENCES_SCHEMAS = new Set(['agentsam-cli-preferences-v1', 'agentsam-cli-preferences-v2']);
|
|
8
8
|
|
|
9
9
|
function readJson(filename) {
|
|
10
10
|
try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
|
|
@@ -43,16 +43,43 @@ export function detectCliProject(startDir = process.cwd()) {
|
|
|
43
43
|
|
|
44
44
|
export function cliPreferencesPath(root) { return path.join(path.resolve(root), '.agentsam', 'cli.json'); }
|
|
45
45
|
|
|
46
|
+
function safeModelSnapshot(value) {
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
48
|
+
const provider = String(value.provider || '').trim();
|
|
49
|
+
const providerModelId = String(value.provider_model_id || '').trim();
|
|
50
|
+
if (!provider || !providerModelId) return null;
|
|
51
|
+
return {
|
|
52
|
+
model_key: String(value.model_key || `${provider}:${providerModelId}`),
|
|
53
|
+
provider,
|
|
54
|
+
provider_model_id: providerModelId,
|
|
55
|
+
label: String(value.label || providerModelId),
|
|
56
|
+
availability: value.availability === 'available' ? 'available' : 'unverified',
|
|
57
|
+
availability_source: String(value.availability_source || ''),
|
|
58
|
+
context_window: Number.isFinite(Number(value.context_window)) && Number(value.context_window) > 0 ? Number(value.context_window) : null,
|
|
59
|
+
context_window_source: String(value.context_window_source || 'unknown'),
|
|
60
|
+
max_output_tokens: Number.isFinite(Number(value.max_output_tokens)) && Number(value.max_output_tokens) > 0 ? Number(value.max_output_tokens) : null,
|
|
61
|
+
max_output_tokens_source: String(value.max_output_tokens_source || 'unknown'),
|
|
62
|
+
reasoning_efforts: Array.isArray(value.reasoning_efforts) && value.reasoning_efforts.length ? value.reasoning_efforts.map(String) : ['auto'],
|
|
63
|
+
service_tiers: Array.isArray(value.service_tiers) && value.service_tiers.length ? value.service_tiers.map(String) : ['default'],
|
|
64
|
+
capabilities: value.capabilities && typeof value.capabilities === 'object' ? { ...value.capabilities } : {},
|
|
65
|
+
pricing: value.pricing && typeof value.pricing === 'object' ? { ...value.pricing } : null,
|
|
66
|
+
context_policy: value.context_policy && typeof value.context_policy === 'object' ? { ...value.context_policy } : null,
|
|
67
|
+
source: value.source && typeof value.source === 'object' ? { ...value.source } : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
function normalizePreferences(value = {}) {
|
|
72
|
+
const modelSnapshot = safeModelSnapshot(value.modelSnapshot);
|
|
47
73
|
return {
|
|
48
74
|
schemaVersion: CLI_PREFERENCES_SCHEMA,
|
|
49
75
|
trustedDirectory: value.trustedDirectory === true,
|
|
50
76
|
runtime: value.runtime || 'local',
|
|
51
77
|
terminal: value.terminal || '',
|
|
52
78
|
modelPreference: value.modelPreference || 'auto',
|
|
79
|
+
modelSnapshot,
|
|
53
80
|
reasoningEffort: value.reasoningEffort || 'auto',
|
|
54
81
|
serviceTier: value.serviceTier || 'default',
|
|
55
|
-
modelAuthority: 'preference-only',
|
|
82
|
+
modelAuthority: modelSnapshot?.availability === 'available' ? 'provider-verified' : 'preference-only',
|
|
56
83
|
updatedAt: value.updatedAt || null,
|
|
57
84
|
};
|
|
58
85
|
}
|
|
@@ -60,7 +87,7 @@ function normalizePreferences(value = {}) {
|
|
|
60
87
|
export function readCliPreferences(root) {
|
|
61
88
|
const value = readJson(cliPreferencesPath(root));
|
|
62
89
|
if (!value) return null;
|
|
63
|
-
if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && value.schemaVersion
|
|
90
|
+
if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && !LEGACY_CLI_PREFERENCES_SCHEMAS.has(value.schemaVersion)) return null;
|
|
64
91
|
return normalizePreferences(value);
|
|
65
92
|
}
|
|
66
93
|
|