@cargo-ai/cli 1.0.113 → 1.0.115
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/README.md +22 -5
- package/build/commands/auth/agentId.d.ts +67 -0
- package/build/commands/auth/agentId.d.ts.map +1 -0
- package/build/commands/auth/agentId.js +332 -0
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +25 -4
- package/build/commands/auth/loginChannel.d.ts +3 -0
- package/build/commands/auth/loginChannel.d.ts.map +1 -1
- package/build/commands/auth/loginChannel.js +14 -3
- package/build/index.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Using a coding agent? Skip the manual setup — see [Agent Skills](#agent-skills
|
|
|
51
51
|
|
|
52
52
|
## Authentication
|
|
53
53
|
|
|
54
|
-
A single `cargo-ai login` command supports
|
|
54
|
+
A single `cargo-ai login` command supports four channels — pass exactly one of `--email`, `--oauth`, `--agentid` or `--token`:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
# 1. Sign in with a code emailed to you, no browser needed
|
|
@@ -60,13 +60,16 @@ cargo-ai login --email you@company.com
|
|
|
60
60
|
# 2. Sign in via your browser using the OAuth 2.0 Device Authorization Flow
|
|
61
61
|
cargo-ai login --oauth
|
|
62
62
|
|
|
63
|
-
# 3. Sign
|
|
63
|
+
# 3. Sign an agent in through AgentID
|
|
64
|
+
cargo-ai login --agentid
|
|
65
|
+
|
|
66
|
+
# 4. Sign in with an API token you already have
|
|
64
67
|
cargo-ai login --token <your-api-token>
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
`--email` and `--
|
|
70
|
+
`--email`, `--oauth` and `--agentid` all create the account if it does not exist yet, so there is no separate sign-up command.
|
|
68
71
|
|
|
69
|
-
`--email` and `--
|
|
72
|
+
`--email`, `--oauth` and `--agentid` save the signed-in session — a short-lived access token plus the refresh token that renews it — to `~/.config/cargo-ai/credentials.json` (file mode `0600`). The CLI renews it in the background as it expires, so you stay signed in and no API token is created per machine. `--token` saves the API token you supplied instead. The same file backs `cargo-cdk`, so one login serves both.
|
|
70
73
|
|
|
71
74
|
`cargo-ai logout` revokes the session at the identity provider, so signing out is final. A token you supplied yourself with `--token` is only forgotten locally, since it may be in use elsewhere; pass `--keep-token` to force that behaviour for a session too. If a credentials file cannot be deleted — a read-only config directory, or one owned by another user — `logout` fails and names the file rather than reporting a sign-out the next command would contradict.
|
|
72
75
|
|
|
@@ -114,9 +117,23 @@ echo 123456 | cargo-ai login --email you@company.com --code -
|
|
|
114
117
|
|
|
115
118
|
The CLI ships with a built-in OAuth client, so no setup is required.
|
|
116
119
|
|
|
120
|
+
### AgentID sign-in (`--agentid`)
|
|
121
|
+
|
|
122
|
+
`cargo-ai login --agentid` signs an agent in as itself, through the same AgentID connection the web app offers. It does not open the Cargo app.
|
|
123
|
+
|
|
124
|
+
The command prints an Auth0 authorize URL (`connection=agentid`) and listens on `http://127.0.0.1` for the redirect. Open that URL once. A second visit starts a different sign-in. Auth0 sends the browser to `https://auth.agentid.com`. When that page shows an auth token, approve it within 5 minutes:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
agentmail inboxes authorize --inbox-id <inbox> --auth-token <token> --accept-disclosure true
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
AgentID then redirects back to the CLI, which writes the session the same way `--email` and `--oauth` do. A browser that already has an AgentID session for that inbox continues without the token step.
|
|
131
|
+
|
|
132
|
+
The cargo-ai application in Auth0 has to list `http://127.0.0.1:52701/callback` under Allowed Callback URLs, and have the AgentID connection enabled. Auth0 does not accept a wildcard port, so the CLI always uses this one. Until that URL is allowed, the command stops immediately and says so.
|
|
133
|
+
|
|
117
134
|
### Workspace selection
|
|
118
135
|
|
|
119
|
-
`--email` and `--
|
|
136
|
+
`--email`, `--oauth` and `--agentid` authenticate you as a user, so the CLI picks a workspace to save alongside the session:
|
|
120
137
|
|
|
121
138
|
- **No workspaces**: one is created (you are prompted for a name in a TTY, or pass `--workspace-name <name>`).
|
|
122
139
|
- **One workspace**: it is selected automatically.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type IdentityTokens } from "./auth0.js";
|
|
2
|
+
/** Auth0 connection name. Matches the web sign-in button. */
|
|
3
|
+
export declare const AGENTID_CONNECTION = "agentid";
|
|
4
|
+
/**
|
|
5
|
+
* Auth0 refuses a wildcard port in Allowed Callback URLs, so the CLI always
|
|
6
|
+
* listens here. The cargo-ai application must allow this exact URL.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AGENTID_CALLBACK_PORT = 52701;
|
|
9
|
+
export declare function loopbackRedirectUri(port: number): string;
|
|
10
|
+
export declare const AGENTID_CALLBACK_URL: string;
|
|
11
|
+
export declare function buildAgentIdAuthorizeUrl(opts: {
|
|
12
|
+
domain: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
audience: string;
|
|
15
|
+
scope: string;
|
|
16
|
+
redirectUri: string;
|
|
17
|
+
state: string;
|
|
18
|
+
codeChallenge: string;
|
|
19
|
+
}): string;
|
|
20
|
+
export type AuthorizeCheck = {
|
|
21
|
+
ok: true;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The device-code flow cannot name a connection, so AgentID sign-in starts
|
|
28
|
+
* with a normal authorize request and a loopback redirect. Auth0 answers that
|
|
29
|
+
* request before any browser work; a callback that is not allowed, or a
|
|
30
|
+
* connection that is off, fails here instead of after a five-minute wait.
|
|
31
|
+
*
|
|
32
|
+
* A started sign-in looks like exactly one thing: a redirect away from the
|
|
33
|
+
* Auth0 domain, to AgentID. A page served from Auth0 itself — universal login,
|
|
34
|
+
* an error — means the sign-in never left for AgentID.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifyAuthorizeResponse(opts: {
|
|
37
|
+
domain: string;
|
|
38
|
+
status: number;
|
|
39
|
+
location: string | null;
|
|
40
|
+
body: string;
|
|
41
|
+
}): AuthorizeCheck;
|
|
42
|
+
export type CallbackResult = {
|
|
43
|
+
code: string;
|
|
44
|
+
} | {
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
export declare function readCallbackResult(search: string): CallbackResult;
|
|
48
|
+
/**
|
|
49
|
+
* Only a browser navigation that carries this login's `state` and the
|
|
50
|
+
* authorization result may end the wait. The callback port is fixed and
|
|
51
|
+
* documented, so anything else that reaches it — a probe, a preflight, a
|
|
52
|
+
* cross-site request, a redirect left over from an earlier run — has to leave
|
|
53
|
+
* the listener up rather than settle the wait and fail the sign-in. Auth0
|
|
54
|
+
* returns `state` on error redirects too, so a denial still gets through.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isSignInCallback(method: string, search: string, expectedState: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Signs in through the AgentID Auth0 connection and returns the same session
|
|
59
|
+
* tokens the other channels hand to `establishSession`.
|
|
60
|
+
*
|
|
61
|
+
* The command prints an authorize URL on Auth0. Opening it lands on
|
|
62
|
+
* auth.agentid.com, never on the Cargo app. The agent approves the auth token
|
|
63
|
+
* shown on that page with `agentmail inboxes authorize`. AgentID then redirects
|
|
64
|
+
* back to a localhost port this process is listening on.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runAgentIdLogin(): Promise<IdentityTokens>;
|
|
67
|
+
//# sourceMappingURL=agentId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentId.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/agentId.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,YAAY,CAAC;AAEpB,6DAA6D;AAC7D,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAO5C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,eAAO,MAAM,oBAAoB,QAA6C,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAcT;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAS3E;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,cAAc,CA6BjB;AAgCD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAmBjE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAcT;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,cAAc,CAAC,CAkE/D"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
3
|
+
import { getOAuthConfig } from "../../oauthConfig.js";
|
|
4
|
+
import { failWith, info } from "../runHandler.js";
|
|
5
|
+
import { describeError, parseTokenResponse, postToAuth0, readErrorBody, USER_AGENT, } from "./auth0.js";
|
|
6
|
+
/** Auth0 connection name. Matches the web sign-in button. */
|
|
7
|
+
export const AGENTID_CONNECTION = "agentid";
|
|
8
|
+
/** AgentID drops the waiting page after five minutes. */
|
|
9
|
+
const SIGN_IN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
const CALLBACK_PATH = "/callback";
|
|
11
|
+
/**
|
|
12
|
+
* Auth0 refuses a wildcard port in Allowed Callback URLs, so the CLI always
|
|
13
|
+
* listens here. The cargo-ai application must allow this exact URL.
|
|
14
|
+
*/
|
|
15
|
+
export const AGENTID_CALLBACK_PORT = 52701;
|
|
16
|
+
export function loopbackRedirectUri(port) {
|
|
17
|
+
return `http://127.0.0.1:${String(port)}${CALLBACK_PATH}`;
|
|
18
|
+
}
|
|
19
|
+
export const AGENTID_CALLBACK_URL = loopbackRedirectUri(AGENTID_CALLBACK_PORT);
|
|
20
|
+
export function buildAgentIdAuthorizeUrl(opts) {
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
response_type: "code",
|
|
23
|
+
client_id: opts.clientId,
|
|
24
|
+
redirect_uri: opts.redirectUri,
|
|
25
|
+
scope: opts.scope,
|
|
26
|
+
audience: opts.audience,
|
|
27
|
+
connection: AGENTID_CONNECTION,
|
|
28
|
+
state: opts.state,
|
|
29
|
+
code_challenge: opts.codeChallenge,
|
|
30
|
+
code_challenge_method: "S256",
|
|
31
|
+
});
|
|
32
|
+
return `https://${opts.domain}/authorize?${params.toString()}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Auth0 sent the request to its own universal login page. That page is the
|
|
36
|
+
* Cargo sign-in form, which an agent cannot complete, so the connection never
|
|
37
|
+
* took effect.
|
|
38
|
+
*/
|
|
39
|
+
const CONNECTION_NOT_APPLIED = `Auth0 answered with its own sign-in page instead of handing the request to AgentID, so the "${AGENTID_CONNECTION}" connection did not take effect. Enable it on the cargo-ai Auth0 application, then try again.`;
|
|
40
|
+
/**
|
|
41
|
+
* The device-code flow cannot name a connection, so AgentID sign-in starts
|
|
42
|
+
* with a normal authorize request and a loopback redirect. Auth0 answers that
|
|
43
|
+
* request before any browser work; a callback that is not allowed, or a
|
|
44
|
+
* connection that is off, fails here instead of after a five-minute wait.
|
|
45
|
+
*
|
|
46
|
+
* A started sign-in looks like exactly one thing: a redirect away from the
|
|
47
|
+
* Auth0 domain, to AgentID. A page served from Auth0 itself — universal login,
|
|
48
|
+
* an error — means the sign-in never left for AgentID.
|
|
49
|
+
*/
|
|
50
|
+
export function classifyAuthorizeResponse(opts) {
|
|
51
|
+
if (opts.body.includes("Callback URL mismatch") === true) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
message: `Auth0 rejected the CLI callback (${AGENTID_CALLBACK_URL}). On the cargo-ai Auth0 application, add that exact URL to Allowed Callback URLs and enable the AgentID connection, then try again.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (opts.status >= 300 && opts.status < 400 && opts.location !== null) {
|
|
58
|
+
return classifyRedirect(opts.domain, opts.location);
|
|
59
|
+
}
|
|
60
|
+
const detail = htmlError(opts.body);
|
|
61
|
+
if (detail !== undefined) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
message: `Auth0 refused to start AgentID sign-in: ${detail}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (opts.status >= 200 && opts.status < 300) {
|
|
68
|
+
return { ok: false, message: CONNECTION_NOT_APPLIED };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
message: `Auth0 refused to start AgentID sign-in: HTTP ${String(opts.status)}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function classifyRedirect(domain, location) {
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(location, `https://${domain}`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
message: `Auth0 answered the AgentID sign-in request with a redirect the CLI could not read (${location}).`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const error = url.searchParams.get("error");
|
|
87
|
+
if (error !== null) {
|
|
88
|
+
const description = url.searchParams.get("error_description");
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: `Auth0 refused to start AgentID sign-in: ${description !== null ? description : error}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (url.host === domain) {
|
|
95
|
+
return { ok: false, message: CONNECTION_NOT_APPLIED };
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
export function readCallbackResult(search) {
|
|
100
|
+
const params = new URLSearchParams(search.startsWith("?") === true ? search.slice(1) : search);
|
|
101
|
+
const error = params.get("error");
|
|
102
|
+
if (error !== null) {
|
|
103
|
+
const description = params.get("error_description");
|
|
104
|
+
return { error: description !== null ? description : error };
|
|
105
|
+
}
|
|
106
|
+
const code = params.get("code");
|
|
107
|
+
if (code === null) {
|
|
108
|
+
return { error: "The sign-in redirect did not include a code." };
|
|
109
|
+
}
|
|
110
|
+
return { code };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Only a browser navigation that carries this login's `state` and the
|
|
114
|
+
* authorization result may end the wait. The callback port is fixed and
|
|
115
|
+
* documented, so anything else that reaches it — a probe, a preflight, a
|
|
116
|
+
* cross-site request, a redirect left over from an earlier run — has to leave
|
|
117
|
+
* the listener up rather than settle the wait and fail the sign-in. Auth0
|
|
118
|
+
* returns `state` on error redirects too, so a denial still gets through.
|
|
119
|
+
*/
|
|
120
|
+
export function isSignInCallback(method, search, expectedState) {
|
|
121
|
+
if (method !== "GET") {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const params = new URLSearchParams(search.startsWith("?") === true ? search.slice(1) : search);
|
|
125
|
+
if (params.get("state") !== expectedState) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return params.get("error") !== null || params.get("code") !== null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Signs in through the AgentID Auth0 connection and returns the same session
|
|
132
|
+
* tokens the other channels hand to `establishSession`.
|
|
133
|
+
*
|
|
134
|
+
* The command prints an authorize URL on Auth0. Opening it lands on
|
|
135
|
+
* auth.agentid.com, never on the Cargo app. The agent approves the auth token
|
|
136
|
+
* shown on that page with `agentmail inboxes authorize`. AgentID then redirects
|
|
137
|
+
* back to a localhost port this process is listening on.
|
|
138
|
+
*/
|
|
139
|
+
export async function runAgentIdLogin() {
|
|
140
|
+
const config = getOAuthConfig();
|
|
141
|
+
const state = randomBytes(16).toString("base64url");
|
|
142
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
143
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
144
|
+
let listener;
|
|
145
|
+
try {
|
|
146
|
+
listener = await listenForCallback(state);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
failWith(error instanceof Error ? error.message : String(error));
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const redirectUri = loopbackRedirectUri(listener.port);
|
|
153
|
+
const authorizeUrl = buildAgentIdAuthorizeUrl({
|
|
154
|
+
domain: config.domain,
|
|
155
|
+
clientId: config.clientId,
|
|
156
|
+
audience: config.audience,
|
|
157
|
+
scope: config.scope,
|
|
158
|
+
redirectUri,
|
|
159
|
+
state,
|
|
160
|
+
codeChallenge: challenge,
|
|
161
|
+
});
|
|
162
|
+
await assertAuthorizeStarts({ authorizeUrl, domain: config.domain });
|
|
163
|
+
info([
|
|
164
|
+
"",
|
|
165
|
+
"To sign in with AgentID, open this URL once. It goes to Auth0 and then AgentID, not the Cargo app:",
|
|
166
|
+
` ${authorizeUrl}`,
|
|
167
|
+
"",
|
|
168
|
+
"Open it only once. A second visit starts a different AgentID sign-in, and approving that one does not finish this command.",
|
|
169
|
+
"",
|
|
170
|
+
"If that page shows an auth token, approve it within 5 minutes:",
|
|
171
|
+
" agentmail inboxes authorize --inbox-id <inbox> --auth-token <token> --accept-disclosure true",
|
|
172
|
+
"",
|
|
173
|
+
"Waiting for that page to redirect back here...",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n"));
|
|
176
|
+
let callback;
|
|
177
|
+
try {
|
|
178
|
+
callback = await listener.done;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
failWith(error instanceof Error ? error.message : String(error));
|
|
182
|
+
}
|
|
183
|
+
const result = readCallbackResult(callback);
|
|
184
|
+
if ("error" in result) {
|
|
185
|
+
failWith(`AgentID sign-in failed: ${result.error}`);
|
|
186
|
+
}
|
|
187
|
+
return await exchangeAuthorizationCode({
|
|
188
|
+
code: result.code,
|
|
189
|
+
redirectUri,
|
|
190
|
+
codeVerifier: verifier,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
listener.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function assertAuthorizeStarts(opts) {
|
|
198
|
+
let response;
|
|
199
|
+
try {
|
|
200
|
+
response = await fetch(opts.authorizeUrl, {
|
|
201
|
+
method: "GET",
|
|
202
|
+
redirect: "manual",
|
|
203
|
+
headers: { "user-agent": USER_AGENT },
|
|
204
|
+
signal: AbortSignal.timeout(15_000),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
209
|
+
failWith(`Could not reach Auth0 to start AgentID sign-in: ${reason}`);
|
|
210
|
+
}
|
|
211
|
+
const check = classifyAuthorizeResponse({
|
|
212
|
+
domain: opts.domain,
|
|
213
|
+
status: response.status,
|
|
214
|
+
location: response.headers.get("location"),
|
|
215
|
+
body: await response.text(),
|
|
216
|
+
});
|
|
217
|
+
if (check.ok === false) {
|
|
218
|
+
failWith(check.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function exchangeAuthorizationCode(opts) {
|
|
222
|
+
const config = getOAuthConfig();
|
|
223
|
+
const response = await postToAuth0({
|
|
224
|
+
path: "/oauth/token",
|
|
225
|
+
body: new URLSearchParams({
|
|
226
|
+
grant_type: "authorization_code",
|
|
227
|
+
client_id: config.clientId,
|
|
228
|
+
code: opts.code,
|
|
229
|
+
redirect_uri: opts.redirectUri,
|
|
230
|
+
code_verifier: opts.codeVerifier,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
if (response.ok === false) {
|
|
234
|
+
const errorBody = await readErrorBody(response);
|
|
235
|
+
failWith(`Failed to exchange the AgentID sign-in code (HTTP ${String(response.status)}): ${describeError(errorBody)}`, { extra: { oauthError: errorBody } });
|
|
236
|
+
}
|
|
237
|
+
return parseTokenResponse(await response.json());
|
|
238
|
+
}
|
|
239
|
+
function listenForCallback(expectedState) {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
let settle;
|
|
242
|
+
let fail;
|
|
243
|
+
let settled = false;
|
|
244
|
+
const done = new Promise((resolveDone, rejectDone) => {
|
|
245
|
+
settle = resolveDone;
|
|
246
|
+
fail = rejectDone;
|
|
247
|
+
});
|
|
248
|
+
// The wait can reject while Auth0 is still being checked, before the
|
|
249
|
+
// caller awaits `done`. A handler here keeps that from surfacing as an
|
|
250
|
+
// unhandled rejection; awaiting `done` still throws.
|
|
251
|
+
void done.catch(() => undefined);
|
|
252
|
+
const finish = (search) => {
|
|
253
|
+
if (settled === true || settle === undefined) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
settled = true;
|
|
257
|
+
clearTimeout(timeout);
|
|
258
|
+
settle(search);
|
|
259
|
+
};
|
|
260
|
+
const giveUp = (error) => {
|
|
261
|
+
if (settled === true || fail === undefined) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
settled = true;
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
fail(error);
|
|
267
|
+
};
|
|
268
|
+
const server = createServer((req, res) => {
|
|
269
|
+
handleCallbackRequest(req, res, expectedState, finish);
|
|
270
|
+
});
|
|
271
|
+
const timeout = setTimeout(() => {
|
|
272
|
+
giveUp(new Error("AgentID sign-in timed out before the browser returned. The AgentID page expires after 5 minutes."));
|
|
273
|
+
server.close();
|
|
274
|
+
}, SIGN_IN_TIMEOUT_MS);
|
|
275
|
+
server.on("error", (error) => {
|
|
276
|
+
clearTimeout(timeout);
|
|
277
|
+
if (error.code === "EADDRINUSE") {
|
|
278
|
+
reject(new Error(`${AGENTID_CALLBACK_URL} is already in use. Free that port and run the command again.`));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
reject(error);
|
|
282
|
+
});
|
|
283
|
+
server.listen(AGENTID_CALLBACK_PORT, "127.0.0.1", () => {
|
|
284
|
+
const address = server.address();
|
|
285
|
+
if (address === null || typeof address === "string") {
|
|
286
|
+
reject(new Error("Could not bind a localhost port for AgentID sign-in."));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
resolve({
|
|
290
|
+
port: address.port,
|
|
291
|
+
done,
|
|
292
|
+
close: () => {
|
|
293
|
+
clearTimeout(timeout);
|
|
294
|
+
server.close();
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function handleCallbackRequest(req, res, expectedState, finish) {
|
|
301
|
+
const requestUrl = req.url;
|
|
302
|
+
if (requestUrl === undefined) {
|
|
303
|
+
res.writeHead(400);
|
|
304
|
+
res.end();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const url = new URL(requestUrl, "http://127.0.0.1");
|
|
308
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
309
|
+
res.writeHead(404);
|
|
310
|
+
res.end();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const method = req.method === undefined ? "" : req.method;
|
|
314
|
+
if (isSignInCallback(method, url.search, expectedState) === false) {
|
|
315
|
+
res.writeHead(method === "GET" ? 404 : 405);
|
|
316
|
+
res.end();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
320
|
+
res.end("<!doctype html><title>Cargo</title><p>Sign-in finished. You can close this window and return to the terminal.</p>");
|
|
321
|
+
finish(url.search);
|
|
322
|
+
}
|
|
323
|
+
function htmlError(body) {
|
|
324
|
+
const match = body.match(/class="error-message">([\s\S]*?)<\/p>/);
|
|
325
|
+
if (match === null || match[1] === undefined) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
return match[1]
|
|
329
|
+
.replace(/<[^>]+>/g, " ")
|
|
330
|
+
.replace(/\s+/g, " ")
|
|
331
|
+
.trim();
|
|
332
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AA6BxD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAwJN;AAqHD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE,aAAa,GAClB,cAAc,CAchB"}
|
|
@@ -6,6 +6,7 @@ import { clearCredentials, CredentialsNotClearedError, getCredentialsPath, loadC
|
|
|
6
6
|
import { parseBooleanOption } from "../../utils/booleanOption.js";
|
|
7
7
|
import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
|
|
8
8
|
import { clearStartState } from "../start/state.js";
|
|
9
|
+
import { runAgentIdLogin } from "./agentId.js";
|
|
9
10
|
import { runEmailLogin } from "./email.js";
|
|
10
11
|
import { selectLoginChannel } from "./loginChannel.js";
|
|
11
12
|
import { runOAuthDeviceFlow } from "./oauth.js";
|
|
@@ -13,10 +14,11 @@ import { DEFAULT_BASE_URL, establishSession, resolveBaseUrl, } from "./session.j
|
|
|
13
14
|
export function registerAuthCommands(program, getApi) {
|
|
14
15
|
const login = program
|
|
15
16
|
.command("login")
|
|
16
|
-
.description("Sign in to Cargo with an emailed code (--email), via browser (--oauth), or with an existing API token (--token)")
|
|
17
|
+
.description("Sign in to Cargo with an emailed code (--email), via browser (--oauth), with AgentID (--agentid), or with an existing API token (--token)")
|
|
17
18
|
.option("--email <email>", "Sign in with a code emailed to this address; creates the account on first use")
|
|
18
19
|
.option("--code <code>", 'Verification code from the email, or "-" to read it from stdin; omit to have one sent (--email only)')
|
|
19
20
|
.option("--oauth [true|false]", "Sign in via browser using the OAuth 2.0 Device Authorization Flow", parseBooleanOption)
|
|
21
|
+
.option("--agentid [true|false]", "Sign in as an agent through the AgentID connection; prints an Auth0 URL and waits for it to redirect back to this machine", parseBooleanOption)
|
|
20
22
|
.option("--token <token>", "Existing Cargo API token")
|
|
21
23
|
.option("--workspace-name <name>", "Workspace to use, created if no workspace of that name exists (skips the workspace prompt)")
|
|
22
24
|
.option("--workspace-uuid <uuid>", "Workspace UUID to sign in for (skips the workspace prompt)")
|
|
@@ -27,6 +29,7 @@ Examples:
|
|
|
27
29
|
$ cargo-ai login --email you@company.com --code 123456 --workspace-name "Acme GTM"
|
|
28
30
|
$ cargo-ai login --oauth
|
|
29
31
|
$ cargo-ai login --oauth --workspace-uuid 550e8400-e29b-41d4-a716-446655440000
|
|
32
|
+
$ cargo-ai login --agentid
|
|
30
33
|
$ cargo-ai login --token sk_live_abc123 --base-url https://api.custom.io
|
|
31
34
|
|
|
32
35
|
--email and --oauth both create the account if it does not exist yet, so there
|
|
@@ -40,7 +43,14 @@ Signing in with an address that already has an account resolves to its existing
|
|
|
40
43
|
workspace rather than creating one, and --workspace-name reuses a workspace of
|
|
41
44
|
that name when one exists, so this is safe to re-run.
|
|
42
45
|
|
|
43
|
-
--
|
|
46
|
+
--agentid signs an agent in as itself. The command prints an Auth0 URL with
|
|
47
|
+
the AgentID connection and waits on http://127.0.0.1 for the redirect. Open
|
|
48
|
+
that URL once; a second visit starts a different sign-in. It does not open the Cargo app. When
|
|
49
|
+
AgentID shows an auth token, approve it with "agentmail inboxes authorize".
|
|
50
|
+
The cargo-ai Auth0 application must allow http://127.0.0.1:52701/callback and have
|
|
51
|
+
the AgentID connection enabled.
|
|
52
|
+
|
|
53
|
+
--email, --oauth and --agentid save the signed-in session to
|
|
44
54
|
~/.config/cargo-ai/credentials.json and renew it automatically, so you stay
|
|
45
55
|
signed in without a token being created for each machine. CARGO_CONFIG_DIR
|
|
46
56
|
(or XDG_CONFIG_HOME) moves that file; where the home directory is not writable
|
|
@@ -66,6 +76,11 @@ Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) ta
|
|
|
66
76
|
hint: "opens a browser; no code to copy",
|
|
67
77
|
flag: "--oauth",
|
|
68
78
|
},
|
|
79
|
+
{
|
|
80
|
+
label: "AgentID",
|
|
81
|
+
hint: "an agent signs in as itself; approve the token AgentID shows",
|
|
82
|
+
flag: "--agentid",
|
|
83
|
+
},
|
|
69
84
|
{
|
|
70
85
|
label: "Emailed code",
|
|
71
86
|
hint: "no browser needed; signs you up on first use",
|
|
@@ -88,7 +103,7 @@ Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) ta
|
|
|
88
103
|
// for; --keep-token stays the documented spelling for existing scripts.
|
|
89
104
|
.addOption(new Option("--keep-session").hideHelp())
|
|
90
105
|
.addHelpText("after", `
|
|
91
|
-
A session from "cargo-ai login --email" or "--
|
|
106
|
+
A session from "cargo-ai login --email", "--oauth" or "--agentid" is revoked at the identity
|
|
92
107
|
provider, so signing out is final. Tokens you supplied yourself with --token are
|
|
93
108
|
only forgotten locally, since they may be in use elsewhere.
|
|
94
109
|
|
|
@@ -136,7 +151,7 @@ async function runLogin(opts) {
|
|
|
136
151
|
});
|
|
137
152
|
return;
|
|
138
153
|
}
|
|
139
|
-
const tokens = await
|
|
154
|
+
const tokens = await tokensForChannel(selection.channel);
|
|
140
155
|
const workspace = await establishSession({
|
|
141
156
|
tokens,
|
|
142
157
|
baseUrlOverride: opts.baseUrl,
|
|
@@ -154,6 +169,12 @@ async function runLogin(opts) {
|
|
|
154
169
|
tokenType: "session",
|
|
155
170
|
});
|
|
156
171
|
}
|
|
172
|
+
async function tokensForChannel(channel) {
|
|
173
|
+
if (channel === "agentid") {
|
|
174
|
+
return await runAgentIdLogin();
|
|
175
|
+
}
|
|
176
|
+
return await runOAuthDeviceFlow();
|
|
177
|
+
}
|
|
157
178
|
async function runLogout(opts) {
|
|
158
179
|
const credentials = loadCredentials();
|
|
159
180
|
const plan = planRevocation(credentials, opts);
|
|
@@ -2,11 +2,14 @@ import type { EmailLoginOptions } from "./email.js";
|
|
|
2
2
|
export type LoginOptions = EmailLoginOptions & {
|
|
3
3
|
token?: string;
|
|
4
4
|
oauth?: boolean;
|
|
5
|
+
agentid?: boolean;
|
|
5
6
|
};
|
|
6
7
|
export type LoginChannelSelection = {
|
|
7
8
|
channel: "email";
|
|
8
9
|
} | {
|
|
9
10
|
channel: "oauth";
|
|
11
|
+
} | {
|
|
12
|
+
channel: "agentid";
|
|
10
13
|
} | {
|
|
11
14
|
channel: "token";
|
|
12
15
|
token: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CA6D5E"}
|
|
@@ -5,16 +5,24 @@
|
|
|
5
5
|
export function selectLoginChannel(opts) {
|
|
6
6
|
const wantsEmail = opts.email !== undefined;
|
|
7
7
|
const wantsOAuth = opts.oauth === true;
|
|
8
|
+
const wantsAgentId = opts.agentid === true;
|
|
8
9
|
const wantsToken = opts.token !== undefined;
|
|
9
|
-
const selectedCount = [
|
|
10
|
+
const selectedCount = [
|
|
11
|
+
wantsEmail,
|
|
12
|
+
wantsOAuth,
|
|
13
|
+
wantsAgentId,
|
|
14
|
+
wantsToken,
|
|
15
|
+
].filter((selected) => {
|
|
10
16
|
return selected === true;
|
|
11
17
|
}).length;
|
|
12
18
|
if (selectedCount > 1) {
|
|
13
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
error: "Pass only one of --email, --oauth, --agentid or --token.",
|
|
21
|
+
};
|
|
14
22
|
}
|
|
15
23
|
if (selectedCount === 0) {
|
|
16
24
|
return {
|
|
17
|
-
error: "Must pass --email <email> (emailed code), --oauth (browser sign-in) or --token <token> (existing API token).",
|
|
25
|
+
error: "Must pass --email <email> (emailed code), --oauth (browser sign-in), --agentid (AgentID) or --token <token> (existing API token).",
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
28
|
if (opts.code !== undefined && wantsEmail === false) {
|
|
@@ -35,6 +43,9 @@ export function selectLoginChannel(opts) {
|
|
|
35
43
|
if (opts.email !== undefined) {
|
|
36
44
|
return { channel: "email" };
|
|
37
45
|
}
|
|
46
|
+
if (opts.agentid === true) {
|
|
47
|
+
return { channel: "agentid" };
|
|
48
|
+
}
|
|
38
49
|
if (opts.token !== undefined) {
|
|
39
50
|
return { channel: "token", token: opts.token };
|
|
40
51
|
}
|
package/build/index.js
CHANGED
|
@@ -62,6 +62,7 @@ Agent skills:
|
|
|
62
62
|
Authentication:
|
|
63
63
|
Emailed code: cargo-ai login --email you@company.com (no browser; signs you up on first use)
|
|
64
64
|
Browser sign-in: cargo-ai login --oauth
|
|
65
|
+
AgentID: cargo-ai login --agentid
|
|
65
66
|
With API token: cargo-ai login --token <your-token>
|
|
66
67
|
Use env variables: CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL
|
|
67
68
|
Check status: cargo-ai whoami
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cargo-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.115",
|
|
4
|
+
"mcpName": "io.github.getcargohq/cargo",
|
|
4
5
|
"private": false,
|
|
5
6
|
"license": "UNLICENSED",
|
|
6
7
|
"homepage": "https://getcargo.ai",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
"dependencies": {
|
|
74
75
|
"@ai-sdk/harness": "^1.0.144",
|
|
75
76
|
"@ai-sdk/harness-claude-code": "^1.0.148",
|
|
76
|
-
"@cargo-ai/api": "^1.0.
|
|
77
|
+
"@cargo-ai/api": "^1.0.95",
|
|
77
78
|
"@cargo-ai/app-sdk": "^1.0.11",
|
|
78
79
|
"@cargo-ai/cdk": "^1.0.111",
|
|
79
80
|
"@cargo-ai/types": "^1.0.96",
|