@moda-ai/cli 1.13.0 → 1.13.1
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/dist/auth-storage.d.ts +35 -0
- package/dist/auth-storage.js +159 -0
- package/dist/auth.d.ts +17 -13
- package/dist/auth.js +332 -176
- package/dist/cli-session.d.ts +26 -0
- package/dist/cli-session.js +169 -0
- package/dist/init/index.js +8 -3
- package/dist/init/tenant.js +18 -7
- package/dist/profile-commands.js +10 -1
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +116 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CliAuthError } from './errors.js';
|
|
2
|
+
import { type AuthSessionOptions, type StoredAuth } from './auth-storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fire-and-forget sliding-expiry renewal, invoked on every authenticated
|
|
5
|
+
* request: a no-op unless the session is a moda_ct token inside the renewal
|
|
6
|
+
* window. Never awaited by callers, never throws, never holds the process
|
|
7
|
+
* open (unref'd socket); on success the stored expires_at is rewritten in
|
|
8
|
+
* place.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scheduleRenewalIfNearExpiry(session: StoredAuth, authOptions: AuthSessionOptions): void;
|
|
11
|
+
/** The CliAuthError thrown whenever a stored moda_ct session has expired. */
|
|
12
|
+
export declare function cliSessionExpiredError(): CliAuthError;
|
|
13
|
+
/**
|
|
14
|
+
* Classify a 401 for a moda_ct session. Returns the CliAuthError to throw,
|
|
15
|
+
* or null when the renew probe says the token is actually alive (the caller
|
|
16
|
+
* then surfaces the original response unchanged).
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyModaCtUnauthorized(session: StoredAuth, authOptions: AuthSessionOptions): Promise<CliAuthError | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort server-side revocation of a stored moda_ct session token,
|
|
21
|
+
* called by `moda auth logout` before local deletion. No-op for legacy
|
|
22
|
+
* auth0-kind sessions; all failures are swallowed (the local deletion is
|
|
23
|
+
* the contract).
|
|
24
|
+
*/
|
|
25
|
+
export declare function revokeCliSessionToken(options?: AuthSessionOptions): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=cli-session.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/cli-session.ts — moda_ct CLI session token lifecycle: renewal
|
|
2
|
+
// (sliding expiry), revocation, and 401 classification against the
|
|
3
|
+
// /api/cli/auth/* endpoints.
|
|
4
|
+
import { request as httpRequest } from 'node:http';
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { resolveModaBaseUrl } from './config.js';
|
|
7
|
+
import { CliAuthError } from './errors.js';
|
|
8
|
+
import { isModaCtSession, loadAuthSession, persistAuthSession, } from './auth-storage.js';
|
|
9
|
+
// Background sliding-expiry renewal fires when a moda_ct session has less
|
|
10
|
+
// than this long left.
|
|
11
|
+
const RENEW_WINDOW_SECONDS = 7 * 86_400;
|
|
12
|
+
const RENEW_TIMEOUT_MS = 10_000;
|
|
13
|
+
// Synchronous renew probe used to classify a 401, and best-effort revoke on
|
|
14
|
+
// logout: both must never stall a command, so they get a tight budget.
|
|
15
|
+
const RENEW_PROBE_TIMEOUT_MS = 2_000;
|
|
16
|
+
const REVOKE_TIMEOUT_MS = 2_000;
|
|
17
|
+
/**
|
|
18
|
+
* POST an Authorization-bearer request to a CLI session lifecycle endpoint
|
|
19
|
+
* (/api/cli/auth/renew or /api/cli/auth/revoke) over raw node http/https.
|
|
20
|
+
* Not fetch: the background renewal must never delay process exit, and only
|
|
21
|
+
* a raw socket can be unref'd. Rejects on network failure or timeout.
|
|
22
|
+
*/
|
|
23
|
+
function postCliSessionEndpoint(path, token, timeoutMs, opts = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
// Resolve the origin from the same profile/env context the session was
|
|
28
|
+
// loaded with: a `--profile staging` session's renew/revoke must hit the
|
|
29
|
+
// staging backend that minted it, not the active/default profile's.
|
|
30
|
+
url = new URL(`${resolveModaBaseUrl(opts.authOptions)}${path}`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
37
|
+
const req = requestFn(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
40
|
+
timeout: timeoutMs,
|
|
41
|
+
}, (res) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
44
|
+
res.on('end', () => {
|
|
45
|
+
resolve({
|
|
46
|
+
status: res.statusCode ?? 0,
|
|
47
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
res.on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
if (opts.unrefSocket) {
|
|
53
|
+
req.on('socket', (socket) => {
|
|
54
|
+
socket.unref();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
req.on('timeout', () => {
|
|
58
|
+
req.destroy(new Error(`CLI session request timed out after ${timeoutMs}ms`));
|
|
59
|
+
});
|
|
60
|
+
req.on('error', reject);
|
|
61
|
+
req.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a renew response body and rewrite the stored session's expiry in
|
|
66
|
+
* place, preserving the profile scoping the session was loaded with.
|
|
67
|
+
* Best-effort: an unreadable or incomplete body is simply ignored.
|
|
68
|
+
*/
|
|
69
|
+
function persistRenewedExpiry(session, body, authOptions) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(body);
|
|
72
|
+
const expiresAtMs = typeof parsed.expires_at === 'string' ? Date.parse(parsed.expires_at) : NaN;
|
|
73
|
+
if (!Number.isFinite(expiresAtMs))
|
|
74
|
+
return;
|
|
75
|
+
persistAuthSession({ ...session, expires_at: Math.floor(expiresAtMs / 1000) }, authOptions);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best-effort persistence only.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Stampede guard: at most one background renewal in flight per process.
|
|
82
|
+
let renewInFlight = false;
|
|
83
|
+
/**
|
|
84
|
+
* Fire-and-forget sliding-expiry renewal, invoked on every authenticated
|
|
85
|
+
* request: a no-op unless the session is a moda_ct token inside the renewal
|
|
86
|
+
* window. Never awaited by callers, never throws, never holds the process
|
|
87
|
+
* open (unref'd socket); on success the stored expires_at is rewritten in
|
|
88
|
+
* place.
|
|
89
|
+
*/
|
|
90
|
+
export function scheduleRenewalIfNearExpiry(session, authOptions) {
|
|
91
|
+
if (!isModaCtSession(session))
|
|
92
|
+
return;
|
|
93
|
+
if (session.expires_at - Date.now() / 1000 >= RENEW_WINDOW_SECONDS)
|
|
94
|
+
return;
|
|
95
|
+
if (renewInFlight)
|
|
96
|
+
return;
|
|
97
|
+
renewInFlight = true;
|
|
98
|
+
void (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await postCliSessionEndpoint('/api/cli/auth/renew', session.access_token, RENEW_TIMEOUT_MS, { unrefSocket: true, authOptions });
|
|
101
|
+
if (result.status !== 200)
|
|
102
|
+
return;
|
|
103
|
+
persistRenewedExpiry(session, result.body, authOptions);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Silent by design: renewal is opportunistic, the command result must
|
|
107
|
+
// not depend on it.
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
renewInFlight = false;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
/** The CliAuthError thrown whenever a stored moda_ct session has expired. */
|
|
115
|
+
export function cliSessionExpiredError() {
|
|
116
|
+
return new CliAuthError('The stored CLI session token has expired. Run "moda auth login" to sign in again.', { reason: 'token_expired', login_command: ['moda', 'auth', 'login'] });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Classify a 401 for a moda_ct session. Returns the CliAuthError to throw,
|
|
120
|
+
* or null when the renew probe says the token is actually alive (the caller
|
|
121
|
+
* then surfaces the original response unchanged).
|
|
122
|
+
*/
|
|
123
|
+
export async function classifyModaCtUnauthorized(session, authOptions) {
|
|
124
|
+
const loginCommand = ['moda', 'auth', 'login'];
|
|
125
|
+
if (Date.now() / 1000 > session.expires_at) {
|
|
126
|
+
return cliSessionExpiredError();
|
|
127
|
+
}
|
|
128
|
+
let probe;
|
|
129
|
+
try {
|
|
130
|
+
probe = await postCliSessionEndpoint('/api/cli/auth/renew', session.access_token, RENEW_PROBE_TIMEOUT_MS, { authOptions });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return new CliAuthError('The Moda API rejected the stored CLI session token, and the renewal check could not reach the server. ' +
|
|
134
|
+
'The token has not expired, so this is likely transient: retry the command instead of logging in again.', { reason: 'renew_failed', login_command: loginCommand });
|
|
135
|
+
}
|
|
136
|
+
if (probe.status === 401) {
|
|
137
|
+
return new CliAuthError('The stored CLI session token has been revoked. Run "moda auth login" to sign in again.', { reason: 'token_revoked', login_command: loginCommand });
|
|
138
|
+
}
|
|
139
|
+
if (probe.status === 200) {
|
|
140
|
+
// The token is demonstrably alive (the probe just slid its expiry) but
|
|
141
|
+
// the original request still 401'd; persist the fresh expiry and let the
|
|
142
|
+
// caller see the original response.
|
|
143
|
+
persistRenewedExpiry(session, probe.body, authOptions);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Any other renew status (e.g. a 5xx) is indistinguishable from a
|
|
147
|
+
// transient server problem: same retry guidance as a network failure.
|
|
148
|
+
return new CliAuthError('The Moda API rejected the stored CLI session token, and the renewal check failed ' +
|
|
149
|
+
`(HTTP ${probe.status}). The token has not expired, so this is likely transient: ` +
|
|
150
|
+
'retry the command instead of logging in again.', { reason: 'renew_failed', login_command: loginCommand });
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Best-effort server-side revocation of a stored moda_ct session token,
|
|
154
|
+
* called by `moda auth logout` before local deletion. No-op for legacy
|
|
155
|
+
* auth0-kind sessions; all failures are swallowed (the local deletion is
|
|
156
|
+
* the contract).
|
|
157
|
+
*/
|
|
158
|
+
export async function revokeCliSessionToken(options = {}) {
|
|
159
|
+
const session = loadAuthSession({ ...options, includeExpired: true });
|
|
160
|
+
if (!session || !isModaCtSession(session))
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await postCliSessionEndpoint('/api/cli/auth/revoke', session.access_token, REVOKE_TIMEOUT_MS, { authOptions: options });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Best-effort: logout proceeds with local deletion regardless.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=cli-session.js.map
|
package/dist/init/index.js
CHANGED
|
@@ -44,8 +44,13 @@ export async function runInit(flags, options = {}) {
|
|
|
44
44
|
emit('welcome', 'Onboarding this project as a Moda agent harness.', 'completed');
|
|
45
45
|
phases.push({ id: 'welcome', title: 'Welcome', status: 'completed' });
|
|
46
46
|
// Step 2: Authentication
|
|
47
|
+
// Every auth touchpoint below (cached-token check, browser login, tenant
|
|
48
|
+
// setup) must share the same --profile context, or `moda init --profile X`
|
|
49
|
+
// logs in / stores under the default profile while the rest of init reads
|
|
50
|
+
// from X — the session then looks unauthenticated or uses the wrong key.
|
|
51
|
+
const profile = stringFlag(flags.profile);
|
|
47
52
|
log(renderInitStep(2, 7, 'Authentication'));
|
|
48
|
-
let token = loadToken();
|
|
53
|
+
let token = loadToken({ profile });
|
|
49
54
|
if (!token) {
|
|
50
55
|
if (json || agent) {
|
|
51
56
|
throw new CliAuthError('Browser authentication is required before non-interactive init can continue.', {
|
|
@@ -66,7 +71,7 @@ export async function runInit(flags, options = {}) {
|
|
|
66
71
|
}
|
|
67
72
|
log(renderInitNote(`Opening browser to ${action === 'signup' ? 'sign up' : 'log in'}...`, 'warning'));
|
|
68
73
|
emit('auth', `Opening browser to ${action}.`);
|
|
69
|
-
token = await login(action);
|
|
74
|
+
token = await login(action, { profile });
|
|
70
75
|
log(renderInitNote('Authentication successful'));
|
|
71
76
|
}
|
|
72
77
|
else {
|
|
@@ -87,7 +92,7 @@ export async function runInit(flags, options = {}) {
|
|
|
87
92
|
nonInteractive: json || agent,
|
|
88
93
|
silent: !human,
|
|
89
94
|
resumeCommand: ['moda', 'init', ...resumeFlagsForInit(flags)],
|
|
90
|
-
profile
|
|
95
|
+
profile,
|
|
91
96
|
});
|
|
92
97
|
log(renderInitNote(`Active workspace: ${tenantName}`));
|
|
93
98
|
emit('workspace', `Active workspace: ${tenantName}`, 'completed');
|
package/dist/init/tenant.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/init/tenant.ts — Tenant selection and API key provisioning
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
|
-
import { authFetch } from '../auth.js';
|
|
3
|
+
import { authFetch, getCachedLoginContext } from '../auth.js';
|
|
4
4
|
import { loadCliConfig, resolveIngestUrl, resolveModaBaseUrl, saveCliConfig, } from '../config.js';
|
|
5
5
|
import { resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, } from '../profiles.js';
|
|
6
6
|
import { CliAccessError, CliInputError, CliInputRequiredError } from '../errors.js';
|
|
@@ -99,13 +99,24 @@ export async function selectTenantAndCreateKey(options = {}) {
|
|
|
99
99
|
profile: options.profile,
|
|
100
100
|
env: options.env,
|
|
101
101
|
};
|
|
102
|
-
// 1. Fetch CLI context
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
// 1. Fetch CLI context. A PKCE login that just ran in this process already
|
|
103
|
+
// returned the user + tenants in the exchange response; reuse it and skip
|
|
104
|
+
// one /api/cli/context round-trip. The cache is profile-keyed (a login for
|
|
105
|
+
// a different profile returns null here), and the fetch stays as the
|
|
106
|
+
// fallback for cached-token runs (no login happened) and legacy logins.
|
|
107
|
+
let ctx;
|
|
108
|
+
const cachedContext = getCachedLoginContext(authOptions);
|
|
109
|
+
if (cachedContext) {
|
|
110
|
+
ctx = cachedContext;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const contextRes = await authFetch('/api/cli/context', {}, authOptions);
|
|
114
|
+
if (!contextRes.ok) {
|
|
115
|
+
const body = await contextRes.text();
|
|
116
|
+
throw new Error(`Failed to fetch context (${contextRes.status}): ${body}`);
|
|
117
|
+
}
|
|
118
|
+
ctx = (await contextRes.json());
|
|
107
119
|
}
|
|
108
|
-
const ctx = (await contextRes.json());
|
|
109
120
|
log(` Authenticated as ${ctx.user.email}`);
|
|
110
121
|
// 2. Select tenant
|
|
111
122
|
if (ctx.tenants.length === 0) {
|
package/dist/profile-commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { authFetch, clearAuthSession, isAuthSessionValid, loadAuthSession, login } from './auth.js';
|
|
1
|
+
import { authFetch, clearAuthSession, isAuthSessionValid, loadAuthSession, login, revokeCliSessionToken, } from './auth.js';
|
|
2
2
|
import { EXIT_AUTH_REQUIRED, EXIT_OK } from './output.js';
|
|
3
3
|
import { KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS, createLocalKeyDescriptor, loadLocalKeyDescriptors, upsertLocalKeyDescriptor, writeLocalApiKeySecret, } from './keys.js';
|
|
4
4
|
import { DEFAULT_INGEST_URL, PROD_BASE_URL, ensureSecretFilePermissions, getActiveProfile, getModaPaths, loadProfileConfig, resolveApiKeyFromActiveProfile, resolveSecretReference, secretFilePathFromRef, setActiveProfile, upsertProfile, } from './profiles.js';
|
|
@@ -72,6 +72,10 @@ export async function runAuthCommand(context) {
|
|
|
72
72
|
return { exitCode: 0 };
|
|
73
73
|
}
|
|
74
74
|
case 'logout': {
|
|
75
|
+
// moda_ct session tokens are server-side revocable: best-effort revoke
|
|
76
|
+
// (bounded, catch-all inside) before the local deletion. Legacy
|
|
77
|
+
// auth0-kind sessions have nothing to revoke.
|
|
78
|
+
await revokeCliSessionToken({ profile: context.profile, env: context.env });
|
|
75
79
|
clearAuthSession({ profile: context.profile, env: context.env });
|
|
76
80
|
context.output.writeData({
|
|
77
81
|
status: 'logged_out',
|
|
@@ -119,6 +123,11 @@ export async function runAuthCommand(context) {
|
|
|
119
123
|
? new Date(session.expires_at * 1000).toISOString()
|
|
120
124
|
: null,
|
|
121
125
|
};
|
|
126
|
+
// `user` is emitted only when the stored session carries an email —
|
|
127
|
+
// the PKCE exchange path persists user_email; legacy sessions lack it.
|
|
128
|
+
if (session?.user_email) {
|
|
129
|
+
data.user = { email: session.user_email };
|
|
130
|
+
}
|
|
122
131
|
if (boolFlag(context, 'online')) {
|
|
123
132
|
let online = { reachable: false };
|
|
124
133
|
try {
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -78,6 +78,53 @@ npx -p @moda-ai/cli moda <command>
|
|
|
78
78
|
export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
### Auth session inspection and token composition
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
moda auth login # browser PKCE sign-in; stores a moda_ct_ session token
|
|
85
|
+
moda auth logout # revoke the session server-side, then delete it locally
|
|
86
|
+
moda auth status # local session state; exit 0 logged in, exit 4 logged out
|
|
87
|
+
moda auth status --online # additionally probe GET /api/cli/context
|
|
88
|
+
moda auth token # print the raw bearer token + newline, nothing else
|
|
89
|
+
curl -H "Authorization: Bearer $(moda auth token)" https://moda.dev/api/cli/context
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `moda auth login` opens the browser consent flow at `{MODA_BASE_URL}/auth/cli`
|
|
93
|
+
with a localhost callback (ports 8432-8435, first free). Approving in the
|
|
94
|
+
browser hands the CLI a one-time code that it exchanges for a `moda_ct_...`
|
|
95
|
+
session token (sliding 30-day expiry, renewed automatically in the
|
|
96
|
+
background while you keep using the CLI). If the browser cannot reach the
|
|
97
|
+
machine, paste the callback URL or the code shown on the consent page into
|
|
98
|
+
the terminal.
|
|
99
|
+
- `moda auth logout` revokes the stored `moda_ct_` session server-side
|
|
100
|
+
(best-effort) and deletes the local credential; legacy sessions are just
|
|
101
|
+
deleted locally.
|
|
102
|
+
- `moda auth status` reports
|
|
103
|
+
`{ "loggedIn": <bool>, "profile": <string|null>, "expiresAt": <ISO|null> }`
|
|
104
|
+
plus `"user": { "email": <string> }` when the stored session carries an
|
|
105
|
+
email (PKCE `moda_ct_` sessions do; legacy sessions omit it), and exits `4`
|
|
106
|
+
when logged out. That exit is an **expected state signal, not
|
|
107
|
+
an error**: the payload is clean data (no error block, no stderr noise), and
|
|
108
|
+
no browser is ever opened. With `--online` the payload adds
|
|
109
|
+
`{ "online": { "reachable": <bool>, "ok": <bool>, "tenantCount": <n> } }`:
|
|
110
|
+
`reachable: true` means any HTTP response arrived (even a 401/500), `ok`
|
|
111
|
+
reflects HTTP success, and `tenantCount` is present only when the response
|
|
112
|
+
was ok and carried a tenants array. A network failure or probe timeout
|
|
113
|
+
(5 s) yields `reachable: false`; the exit code always follows local session
|
|
114
|
+
validity only, never the probe.
|
|
115
|
+
- `moda auth token` writes exactly the bare token plus a trailing newline to
|
|
116
|
+
stdout in **every** output mode — including piped/json/agent, because
|
|
117
|
+
command substitution is a pipe — so `$(moda auth token)` composes safely
|
|
118
|
+
with no envelope to strip. When logged out it prints a stderr message and
|
|
119
|
+
exits `4`; with an explicit `--json` flag it instead prints
|
|
120
|
+
`{"error":"not_authenticated"}` to stdout (still exit `4`).
|
|
121
|
+
- `moda auth token` prints whichever credential kind is stored. Newer logins
|
|
122
|
+
store a `moda_ct_...` CLI session token, which authenticates **`/api/cli/*`
|
|
123
|
+
endpoints only** — it is not accepted by the Data API (`/api/v1/data/*`
|
|
124
|
+
wants a `moda_sk_...` API key) or other dashboard endpoints. Legacy
|
|
125
|
+
sessions print the Auth0 bearer token as before.
|
|
126
|
+
- Both respect `--profile=NAME` and `MODA_PROFILE`.
|
|
127
|
+
|
|
81
128
|
## Agent protocol
|
|
82
129
|
|
|
83
130
|
Run `moda manifest --json` first when integrating with the CLI. It returns the
|
|
@@ -117,10 +164,73 @@ Branch on the exit code; it is stable and cheaper than parsing the payload:
|
|
|
117
164
|
| `0` | ok — answer is grounded in Moda Cloud intelligence | Trust it. |
|
|
118
165
|
| `1` | error — bad input, API/network failure, or unexpected failure | Read `errors[]`; retry only when the code is retryable. |
|
|
119
166
|
| `3` | degraded — Moda Cloud was unavailable; answer synthesized from local Data API evidence | Answer is present but lower-trust; verify before acting. |
|
|
120
|
-
| `4` | auth required | Run `
|
|
121
|
-
| `5` | input required | Pick a choice
|
|
167
|
+
| `4` | auth required — credentials are missing or expired | Run the `login_command` from the `auth` payload (`moda auth login`), then re-run `resume_command`. |
|
|
168
|
+
| `5` | input required — a choice is needed that the non-interactive run could not answer | Pick a choice from the `input_request` payload and re-invoke the same command with that choice's `resume_flags` appended. |
|
|
122
169
|
| `130` | cancelled | Treat as interrupted or closed pipe; do not retry blindly. |
|
|
123
170
|
|
|
171
|
+
Exit codes `4` and `5` are recoverable-by-caller failures: the envelope's
|
|
172
|
+
`errors[]` entry carries the exact commands/flags to recover with, as argv
|
|
173
|
+
arrays (execute element-wise; join with spaces only for display).
|
|
174
|
+
|
|
175
|
+
#### `auth_required` payload (exit `4`)
|
|
176
|
+
|
|
177
|
+
The envelope's `errors[]` entry carries the recovery data on its `auth` field
|
|
178
|
+
(in `--agent --stream` mode the same error arrives as an NDJSON event with
|
|
179
|
+
`"event": "auth_required"`):
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"code": "auth_required",
|
|
184
|
+
"message": "Browser authentication is required before non-interactive init can continue.",
|
|
185
|
+
"suggested_commands": ["moda auth login", "moda init --yes"],
|
|
186
|
+
"blocking": true,
|
|
187
|
+
"auth": {
|
|
188
|
+
"reason": "no_stored_credentials",
|
|
189
|
+
"login_command": ["moda", "auth", "login"],
|
|
190
|
+
"resume_command": ["moda", "init", "--yes"]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`reason` is one of:
|
|
196
|
+
|
|
197
|
+
- `no_stored_credentials` — no session stored. Run `login_command`, then
|
|
198
|
+
`resume_command` (when present).
|
|
199
|
+
- `token_expired` — the stored session expired. Run `login_command`, then
|
|
200
|
+
`resume_command`.
|
|
201
|
+
- `token_revoked` — the stored `moda_ct_` CLI session token was revoked
|
|
202
|
+
server-side (e.g. from the dashboard). Run `login_command`, then
|
|
203
|
+
`resume_command`.
|
|
204
|
+
- `renew_failed` — the API rejected an **unexpired** `moda_ct_` token but the
|
|
205
|
+
renewal check could not reach the server, so this is likely transient:
|
|
206
|
+
**retry the command; do not re-login.**
|
|
207
|
+
|
|
208
|
+
#### `needs_input` payload (exit `5`)
|
|
209
|
+
|
|
210
|
+
The envelope's `errors[]` entry carries the recovery data on its
|
|
211
|
+
`input_request` field (in `--agent --stream` mode the event type is
|
|
212
|
+
`"needs_input"`):
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"code": "needs_input",
|
|
217
|
+
"message": "An organization must be selected to continue.",
|
|
218
|
+
"blocking": true,
|
|
219
|
+
"input_request": {
|
|
220
|
+
"id": "init_tenant_selection",
|
|
221
|
+
"question": "Select organization",
|
|
222
|
+
"kind": "select",
|
|
223
|
+
"choices": [
|
|
224
|
+
{ "value": "tnt_a", "label": "Acme (owner)", "resume_flags": ["--tenant-id", "tnt_a"] },
|
|
225
|
+
{ "value": "tnt_b", "label": "Beta (viewer)", "resume_flags": ["--tenant-id", "tnt_b"] }
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Re-invoke the original command with the chosen `resume_flags` appended (here:
|
|
232
|
+
`moda init --tenant-id tnt_a`).
|
|
233
|
+
|
|
124
234
|
### The `moda.agent.v1` envelope
|
|
125
235
|
|
|
126
236
|
Fields an agent should read:
|
|
@@ -584,6 +694,10 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
584
694
|
| Command | Purpose |
|
|
585
695
|
|---|---|
|
|
586
696
|
| `moda init` | Browser auth + provision API key + install agent rules |
|
|
697
|
+
| `moda auth login` | Browser PKCE sign-in; stores a revocable `moda_ct_` session token |
|
|
698
|
+
| `moda auth logout` | Revoke the CLI session server-side, then delete it locally |
|
|
699
|
+
| `moda auth status` | Local auth session state (exit `4` = logged out — expected, not an error) |
|
|
700
|
+
| `moda auth token` | Print the raw bearer token for `$(moda auth token)` composition |
|
|
587
701
|
| `moda search "<query>"` | **Message-grain semantic/keyword/hybrid search (incl. tool calls); primary content-discovery path** |
|
|
588
702
|
| `moda overview` | Dashboard KPIs, top clusters, recent activity |
|
|
589
703
|
| `moda ask "<question>"` | Natural-language production/harness answer with evidence (exit `3` = degraded local fallback) |
|