overlord-cli 4.5.0 → 4.7.0

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/bin/_cli/auth.mjs CHANGED
@@ -9,8 +9,10 @@ import {
9
9
  buildAuthHeaders,
10
10
  clearCredentials,
11
11
  getDefaultOverlordUrl,
12
+ getAuthStatus,
12
13
  loadCredentials,
13
14
  loadRuntime,
15
+ repairCredentials,
14
16
  saveCredentials
15
17
  } from './credentials.mjs';
16
18
 
@@ -460,7 +462,30 @@ export async function authLogin() {
460
462
  console.log('Logged in successfully!');
461
463
  }
462
464
 
463
- export function authStatus() {
465
+ function printVerboseAuthStatus() {
466
+ const status = getAuthStatus();
467
+ if (!status.isLoggedIn) {
468
+ console.log('Not logged in. Run: ovld auth login');
469
+ } else {
470
+ console.log('Logged in');
471
+ }
472
+ console.log(` Platform URL: ${status.platformUrl}`);
473
+ console.log(` Platform source: ${status.platformUrlSource}`);
474
+ console.log(` Token source: ${status.tokenSource}`);
475
+ console.log(` Token present: ${status.tokenPresent ? 'yes' : 'no'}`);
476
+ console.log(` Local secret: ${status.hasLocalSecret ? 'yes' : 'no'}`);
477
+ console.log(` credentials.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
478
+ console.log(
479
+ ` electron-credentials.json: ${status.electronCredentialsFileExists ? 'present' : 'missing'}`
480
+ );
481
+ }
482
+
483
+ export function authStatus(args = []) {
484
+ if (args.includes('--verbose') || args.includes('-v')) {
485
+ printVerboseAuthStatus();
486
+ return;
487
+ }
488
+
464
489
  const creds = loadCredentials();
465
490
  if (!creds) {
466
491
  console.log('Not logged in. Run: ovld auth login');
@@ -478,13 +503,24 @@ export function authLogout() {
478
503
  console.log('Logged out.');
479
504
  }
480
505
 
481
- export async function runAuthCommand(subcommand) {
506
+ export function authRepair() {
507
+ const result = repairCredentials();
508
+ if (result.repaired) {
509
+ console.log('Credentials repaired.');
510
+ } else {
511
+ console.log(`Credentials not repaired: ${result.reason}`);
512
+ }
513
+ printVerboseAuthStatus();
514
+ }
515
+
516
+ export async function runAuthCommand(subcommand, args = []) {
482
517
  if (!subcommand || subcommand === 'help' || subcommand === '--help') {
483
518
  console.log(`ovld auth <subcommand>
484
519
 
485
520
  Subcommands:
486
521
  login Authorize the CLI via browser (works locally or over SSH)
487
- status Show current login status
522
+ status Show current login status (use --verbose for redacted diagnostics)
523
+ repair Mirror and chmod shared Desktop/CLI credentials when possible
488
524
  logout Remove stored credentials
489
525
  `);
490
526
  return;
@@ -496,7 +532,12 @@ Subcommands:
496
532
  }
497
533
 
498
534
  if (subcommand === 'status') {
499
- authStatus();
535
+ authStatus(args);
536
+ return;
537
+ }
538
+
539
+ if (subcommand === 'repair') {
540
+ authRepair();
500
541
  return;
501
542
  }
502
543
 
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const CREDENTIALS_DIR = path.join(os.homedir(), '.ovld');
11
11
  const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
12
+ const ELECTRON_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'electron-credentials.json');
12
13
  const RUNTIME_FILE_PATTERN = /^runtime\..+\.json$/;
13
14
  const HOSTED_OVERLORD_URL = 'https://www.ovld.ai';
14
15
  const LOCAL_DEV_OVERLORD_URL = 'http://localhost:3000';
@@ -18,30 +19,121 @@ const LOCAL_SECRET_HEADER = 'X-Overlord-Local-Secret';
18
19
  * @typedef {{ access_token: string, platform_url: string, user_email?: string }} Credentials
19
20
  */
20
21
 
21
- /** @returns {Credentials | null} */
22
- export function loadCredentials() {
22
+ function ensureCredentialsDir() {
23
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
24
+ try {
25
+ fs.chmodSync(CREDENTIALS_DIR, 0o700);
26
+ } catch {
27
+ // Best-effort hardening for existing directories.
28
+ }
29
+ }
30
+
31
+ function readJsonFile(filePath) {
23
32
  try {
24
- const raw = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
33
+ const raw = fs.readFileSync(filePath, 'utf8');
25
34
  return JSON.parse(raw);
26
35
  } catch {
27
36
  return null;
28
37
  }
29
38
  }
30
39
 
40
+ function fileExists(filePath) {
41
+ try {
42
+ return fs.statSync(filePath).isFile();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function writeJsonFileAtomic(filePath, data) {
49
+ ensureCredentialsDir();
50
+ const tempFile = `${filePath}.tmp-${process.pid}-${Date.now()}`;
51
+ fs.writeFileSync(tempFile, JSON.stringify(data, null, 2), { mode: 0o600 });
52
+ fs.renameSync(tempFile, filePath);
53
+ fs.chmodSync(filePath, 0o600);
54
+ }
55
+
56
+ function parseStoredCredentialsData(parsed, { requireAccessToken = false } = {}) {
57
+ if (!parsed || typeof parsed !== 'object') return null;
58
+
59
+ const accessToken = normalizeAgentToken(parsed.access_token);
60
+ const platformUrl = typeof parsed.platform_url === 'string' ? parsed.platform_url.trim() : '';
61
+ if (requireAccessToken && !accessToken) return null;
62
+ if (!accessToken && !platformUrl) return null;
63
+
64
+ return {
65
+ access_token: accessToken,
66
+ platform_url: platformUrl,
67
+ ...(typeof parsed.user_email === 'string' && parsed.user_email.trim()
68
+ ? { user_email: parsed.user_email.trim() }
69
+ : {})
70
+ };
71
+ }
72
+
73
+ function normalizeCredentialsForSave(data) {
74
+ const parsed = parseStoredCredentialsData(data, { requireAccessToken: true });
75
+ if (!parsed) return null;
76
+
77
+ const platformUrl = normalizePlatformUrl(parsed.platform_url);
78
+ if (!platformUrl) return null;
79
+
80
+ return {
81
+ ...parsed,
82
+ platform_url: platformUrl
83
+ };
84
+ }
85
+
86
+ /** @returns {Credentials | null} */
87
+ export function loadCredentials() {
88
+ return (
89
+ parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
90
+ requireAccessToken: true
91
+ }) ??
92
+ parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE))
93
+ );
94
+ }
95
+
31
96
  /** @param {Credentials} data */
32
97
  export function saveCredentials(data) {
33
- fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
34
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
98
+ const credentials = normalizeCredentialsForSave(data);
99
+ if (!credentials) {
100
+ throw new Error('Cannot save empty Overlord credentials.');
101
+ }
102
+
103
+ writeJsonFileAtomic(CREDENTIALS_FILE, credentials);
104
+
105
+ // `electron-credentials.json` is now the shared desktop/CLI credential record.
106
+ // Preserve Electron-only encrypted fields when CLI login refreshes the agent token.
107
+ const existingElectronCredentials = readJsonFile(ELECTRON_CREDENTIALS_FILE);
108
+ const electronPayload =
109
+ existingElectronCredentials && typeof existingElectronCredentials === 'object'
110
+ ? { ...existingElectronCredentials, ...credentials }
111
+ : credentials;
112
+ writeJsonFileAtomic(ELECTRON_CREDENTIALS_FILE, electronPayload);
35
113
  }
36
114
 
37
115
  export function clearCredentials() {
38
- try {
39
- fs.unlinkSync(CREDENTIALS_FILE);
40
- } catch {
41
- // Already gone
116
+ for (const filePath of [CREDENTIALS_FILE, ELECTRON_CREDENTIALS_FILE]) {
117
+ try {
118
+ fs.unlinkSync(filePath);
119
+ } catch {
120
+ // Already gone
121
+ }
42
122
  }
43
123
  }
44
124
 
125
+ function getCredentialFileSource() {
126
+ const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
127
+ requireAccessToken: true
128
+ });
129
+ if (electronCredentials) return 'electron-credentials.json';
130
+
131
+ const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE));
132
+ if (cliCredentials) return 'credentials.json';
133
+
134
+ return 'none';
135
+ }
136
+
45
137
  function getRuntimeFilePath(targetUrl) {
46
138
  try {
47
139
  const parsed = new URL(targetUrl);
@@ -254,6 +346,55 @@ export function resolveAuth() {
254
346
  };
255
347
  }
256
348
 
349
+ export function getAuthStatus() {
350
+ const creds = loadCredentials();
351
+ const resolved = resolveAuth();
352
+
353
+ let tokenSource = 'fallback';
354
+ if (normalizeAgentToken(process.env.AGENT_TOKEN)) {
355
+ tokenSource = 'AGENT_TOKEN';
356
+ } else if (normalizeAgentToken(creds?.access_token)) {
357
+ tokenSource = getCredentialFileSource();
358
+ }
359
+
360
+ let platformUrlSource = 'default';
361
+ if (normalizePlatformUrl(process.env.OVERLORD_URL)) {
362
+ platformUrlSource = 'OVERLORD_URL';
363
+ } else if (normalizeStoredPlatformUrl(creds?.platform_url)) {
364
+ platformUrlSource = getCredentialFileSource();
365
+ }
366
+
367
+ return {
368
+ isLoggedIn: tokenSource !== 'fallback',
369
+ platformUrl: resolved.platformUrl,
370
+ platformUrlSource,
371
+ tokenPresent: tokenSource !== 'fallback',
372
+ tokenSource,
373
+ hasLocalSecret: Boolean(resolved.localSecret),
374
+ credentialsFileExists: fileExists(CREDENTIALS_FILE),
375
+ electronCredentialsFileExists: fileExists(ELECTRON_CREDENTIALS_FILE)
376
+ };
377
+ }
378
+
379
+ export function repairCredentials() {
380
+ const creds = loadCredentials();
381
+ if (!creds || !normalizeAgentToken(creds.access_token)) {
382
+ ensureCredentialsDir();
383
+ return {
384
+ repaired: false,
385
+ reason: 'No valid stored credentials with an access token were found.',
386
+ status: getAuthStatus()
387
+ };
388
+ }
389
+
390
+ saveCredentials(creds);
391
+
392
+ return {
393
+ repaired: true,
394
+ status: getAuthStatus()
395
+ };
396
+ }
397
+
257
398
  function normalizeAgentToken(value) {
258
399
  if (typeof value !== 'string') return '';
259
400
  return value.trim();
@@ -31,7 +31,7 @@ Usage:
31
31
  ${primaryCommand} attach [ticketId] [agent] Search tickets and launch an agent (interactive)
32
32
  ${primaryCommand} create "<objective>" Create a ticket with numbered project selection
33
33
  ${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
34
- ${primaryCommand} auth <subcommand> Login, logout, or check auth status
34
+ ${primaryCommand} auth <subcommand> Login, logout, repair, or check auth status
35
35
  ${primaryCommand} tickets <subcommand> Create or list tickets
36
36
  ${primaryCommand} ticket <subcommand> Work with a single ticket
37
37
  ${primaryCommand} protocol <subcommand> Agent workflow commands
@@ -46,10 +46,12 @@ Usage:
46
46
 
47
47
  Agents:
48
48
  Use ${primaryCommand} protocol help for ticket lifecycle commands.
49
+ Key protocol commands: auth-status, discover-project, spawn, attach, connect, load-context.
49
50
 
50
51
  Auth:
51
52
  ${primaryCommand} auth login Authorize CLI via browser
52
53
  ${primaryCommand} auth status Show login status
54
+ ${primaryCommand} auth repair Repair shared Desktop/CLI credentials
53
55
  ${primaryCommand} auth logout Remove stored credentials
54
56
 
55
57
  Tickets:
@@ -5,7 +5,7 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
 
8
- import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
8
+ import { buildAuthHeaders, getAuthStatus, resolveAuth } from './credentials.mjs';
9
9
 
10
10
  /**
11
11
  * Parse simple CLI flags: --key value or --key=value
@@ -51,6 +51,36 @@ export function resolveProtocolTicketDelegate(flags = {}, agentIdentifier = '')
51
51
  return resolvedAgent || null;
52
52
  }
53
53
 
54
+ export function resolveProtocolModelIdentifier(flags = {}) {
55
+ const explicitModel = typeof flags.model === 'string' ? flags.model.trim() : '';
56
+ if (explicitModel) return explicitModel;
57
+
58
+ const envModel =
59
+ process.env.OVERLORD_MODEL_IDENTIFIER?.trim() ||
60
+ process.env.MODEL_IDENTIFIER?.trim() ||
61
+ process.env.AGENT_MODEL?.trim();
62
+ return envModel || null;
63
+ }
64
+
65
+ function resolveProtocolMetadata(flags = {}, base = {}) {
66
+ const metadata = { ...base };
67
+
68
+ if (flags['metadata-json']) {
69
+ const parsed = JSON.parse(String(flags['metadata-json']));
70
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
71
+ throw new Error('--metadata-json must be a JSON object');
72
+ }
73
+ Object.assign(metadata, parsed);
74
+ }
75
+
76
+ const modelIdentifier = resolveProtocolModelIdentifier(flags);
77
+ if (modelIdentifier) {
78
+ metadata.model = modelIdentifier;
79
+ }
80
+
81
+ return metadata;
82
+ }
83
+
54
84
  /**
55
85
  * Default request timeout in milliseconds. Overridable via --timeout flag or
56
86
  * OVERLORD_TIMEOUT env var. A bounded timeout prevents indefinite spinner hangs
@@ -446,9 +476,7 @@ async function protocolAttach(args) {
446
476
  agentIdentifier: resolveProtocolAgentIdentifier(flags),
447
477
  connectionMethod: String(flags.method ?? 'cli'),
448
478
  ...(externalSessionId !== undefined ? { externalSessionId } : {}),
449
- metadata: {
450
- cwd: process.cwd()
451
- }
479
+ metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() })
452
480
  };
453
481
 
454
482
  const data = await apiPost(
@@ -589,6 +617,31 @@ async function protocolAsk(args) {
589
617
  console.log(JSON.stringify(data, null, 2));
590
618
  }
591
619
 
620
+ // ---------------------------------------------------------------------------
621
+ // permission-request
622
+ // ---------------------------------------------------------------------------
623
+
624
+ async function protocolPermissionRequest(args) {
625
+ const flags = parseFlags(args);
626
+ const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
627
+ const payload = flags['payload-file']
628
+ ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
629
+ : {};
630
+
631
+ const { platformUrl, agentToken, localSecret } = resolveAuth();
632
+ const timeoutMs = resolveTimeout(flags);
633
+
634
+ const data = await apiPost(
635
+ platformUrl,
636
+ agentToken,
637
+ localSecret,
638
+ `/api/protocol/permission-request?ticketId=${encodeURIComponent(ticketId)}`,
639
+ payload,
640
+ timeoutMs
641
+ );
642
+ console.log(JSON.stringify(data, null, 2));
643
+ }
644
+
592
645
  // ---------------------------------------------------------------------------
593
646
  // read-context
594
647
  // ---------------------------------------------------------------------------
@@ -933,7 +986,7 @@ async function protocolConnect(args) {
933
986
  ticketId,
934
987
  agentIdentifier: resolveProtocolAgentIdentifier(flags),
935
988
  connectionMethod: String(flags.method ?? 'cli'),
936
- metadata: {}
989
+ metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() })
937
990
  };
938
991
 
939
992
  const data = await apiPost(
@@ -995,7 +1048,7 @@ async function protocolSpawn(args) {
995
1048
  objective,
996
1049
  agentIdentifier,
997
1050
  connectionMethod: String(flags.method ?? 'cli'),
998
- metadata: {},
1051
+ metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() }),
999
1052
  ...(flags.title ? { title: String(flags.title) } : {}),
1000
1053
  ...(flags.priority ? { priority: String(flags.priority) } : {}),
1001
1054
  ...(flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
@@ -1029,6 +1082,34 @@ async function protocolSpawn(args) {
1029
1082
  }
1030
1083
  }
1031
1084
 
1085
+ // ---------------------------------------------------------------------------
1086
+ // auth-status (agent-friendly auth diagnostics)
1087
+ // ---------------------------------------------------------------------------
1088
+
1089
+ async function protocolAuthStatus() {
1090
+ const status = getAuthStatus();
1091
+
1092
+ console.log(
1093
+ JSON.stringify(
1094
+ {
1095
+ ok: status.isLoggedIn,
1096
+ authStatus: {
1097
+ isLoggedIn: status.isLoggedIn,
1098
+ platformUrl: status.platformUrl,
1099
+ platformUrlSource: status.platformUrlSource,
1100
+ tokenSource: status.tokenSource,
1101
+ tokenPresent: status.tokenPresent,
1102
+ hasLocalSecret: status.hasLocalSecret,
1103
+ credentialsFileExists: status.credentialsFileExists,
1104
+ electronCredentialsFileExists: status.electronCredentialsFileExists
1105
+ }
1106
+ },
1107
+ null,
1108
+ 2
1109
+ )
1110
+ );
1111
+ }
1112
+
1032
1113
  // ---------------------------------------------------------------------------
1033
1114
  // Router
1034
1115
  // ---------------------------------------------------------------------------
@@ -1053,6 +1134,7 @@ Project discovery:
1053
1134
  Use --project-id to override automatic resolution on spawn or ticket creation.
1054
1135
 
1055
1136
  Subcommands:
1137
+ auth-status Return machine-readable auth status for agent runtimes
1056
1138
  discover-project Resolve a project from the current working directory
1057
1139
  attach Start a ticket session and return full working context
1058
1140
  connect Start a lightweight session without full context
@@ -1061,6 +1143,7 @@ Subcommands:
1061
1143
  update Post progress, activity events, and optional change rationales
1062
1144
  record-change-rationales Persist structured change rationales without a progress update
1063
1145
  ask Post a blocking question and move the ticket to review
1146
+ permission-request Notify Overlord that the agent is requesting tool permission
1064
1147
  read-context Read shared persistent context for this ticket
1065
1148
  write-context Write shared persistent context for future sessions
1066
1149
  deliver Finish work, send artifacts, and move the ticket to review
@@ -1072,7 +1155,7 @@ Subcommands:
1072
1155
  Environment fallback:
1073
1156
  --session-key <- SESSION_KEY
1074
1157
  --ticket-id <- TICKET_ID
1075
- auth/host <- OVERLORD_URL, AGENT_TOKEN
1158
+ auth/host <- OVERLORD_URL, AGENT_TOKEN, or shared credentials from ovld auth/Desktop login
1076
1159
  --timeout <- OVERLORD_TIMEOUT
1077
1160
 
1078
1161
  Common flags:
@@ -1080,8 +1163,15 @@ Common flags:
1080
1163
  --ticket-id <id> Ticket id when the subcommand operates on an existing ticket
1081
1164
  --session-key <key> Session key returned by attach/connect/spawn
1082
1165
  --agent <identifier> Agent identifier sent to Overlord (default: AGENT_IDENTIFIER or claude-code)
1166
+ --model <identifier> Model identifier to snapshot on executing objectives
1083
1167
  --method <connectionMethod> Connection method sent to Overlord (default: cli)
1084
1168
 
1169
+ auth-status:
1170
+ Purpose:
1171
+ Check whether the local runtime has usable Overlord credentials.
1172
+ Returns:
1173
+ JSON with ok=true|false plus authStatus fields describing token and host sources.
1174
+
1085
1175
  discover-project:
1086
1176
  Purpose:
1087
1177
  Resolve the Overlord project that corresponds to the current (or given) working directory.
@@ -1101,8 +1191,10 @@ attach:
1101
1191
  --ticket-id <id>
1102
1192
  Optional:
1103
1193
  --agent <identifier>
1194
+ --model <identifier>
1104
1195
  --method <connectionMethod>
1105
1196
  --external-session-id <id|null> Store the native agent thread/session id, or clear it with null
1197
+ --metadata-json <json> Extra session metadata object
1106
1198
  Returns:
1107
1199
  Full JSON including session.sessionKey, ticket, history, artifacts, sharedState, and promptContext
1108
1200
  Notes:
@@ -1115,7 +1207,9 @@ connect:
1115
1207
  --ticket-id <id>
1116
1208
  Optional:
1117
1209
  --agent <identifier>
1210
+ --model <identifier>
1118
1211
  --method <connectionMethod>
1212
+ --metadata-json <json> Extra session metadata object
1119
1213
  Returns:
1120
1214
  Session JSON and SESSION_KEY on stderr when available
1121
1215
 
@@ -1167,6 +1261,15 @@ ask:
1167
1261
  Notes:
1168
1262
  After ask succeeds, stop working until the human responds
1169
1263
 
1264
+ permission-request:
1265
+ Purpose:
1266
+ Notify Overlord that the local agent runtime is requesting tool permission.
1267
+ This is primarily used by installed permission hooks.
1268
+ Required:
1269
+ --ticket-id <id>
1270
+ Optional:
1271
+ --payload-file <path|-> Hook JSON payload, or stdin when "-"
1272
+
1170
1273
  read-context:
1171
1274
  Purpose:
1172
1275
  Read persistent shared context written by earlier sessions
@@ -1226,7 +1329,9 @@ spawn:
1226
1329
  --parent-session-key <key>
1227
1330
  --parent-ticket-id <id>
1228
1331
  --agent <identifier>
1332
+ --model <identifier>
1229
1333
  --method <connectionMethod>
1334
+ --metadata-json <json> Extra session metadata object
1230
1335
  Returns:
1231
1336
  New ticket/session JSON plus SESSION_KEY and TICKET_ID on stderr when available
1232
1337
 
@@ -1275,6 +1380,7 @@ artifact-upload-file:
1275
1380
  --metadata-json <json>
1276
1381
 
1277
1382
  Examples:
1383
+ ovld protocol auth-status
1278
1384
  ovld protocol discover-project
1279
1385
  ovld protocol discover-project --working-directory /path/to/repo
1280
1386
  ovld protocol spawn --agent codex --objective "Implement feature X" # auto-resolves project from cwd
@@ -1303,6 +1409,7 @@ Examples:
1303
1409
  }
1304
1410
 
1305
1411
  if (subcommand === 'discover-project') { await protocolDiscoverProject(args); return; }
1412
+ if (subcommand === 'auth-status') { await protocolAuthStatus(); return; }
1306
1413
  if (subcommand === 'attach') { await protocolAttach(args); return; }
1307
1414
  if (subcommand === 'connect') { await protocolConnect(args); return; }
1308
1415
  if (subcommand === 'load-context') { await protocolLoadContext(args); return; }
@@ -1314,6 +1421,7 @@ Examples:
1314
1421
  if (subcommand === 'update') { await protocolUpdate(args); return; }
1315
1422
  if (subcommand === 'record-change-rationales') { await protocolRecordChangeRationales(args); return; }
1316
1423
  if (subcommand === 'ask') { await protocolAsk(args); return; }
1424
+ if (subcommand === 'permission-request') { await protocolPermissionRequest(args); return; }
1317
1425
  if (subcommand === 'read-context') { await protocolReadContext(args); return; }
1318
1426
  if (subcommand === 'write-context') { await protocolWriteContext(args); return; }
1319
1427
  if (subcommand === 'deliver') { await protocolDeliver(args); return; }
@@ -195,13 +195,9 @@ ovld protocol artifact-upload-file --session-key <sessionKey> --ticket-id $TICKE
195
195
  const PERMISSION_HOOK_SCRIPT = `#!/bin/bash
196
196
  # Overlord PermissionRequest notification hook (managed by Overlord)
197
197
  BODY=$(cat -)
198
- if [ -n "$OVERLORD_URL" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$TICKET_ID" ]; then
199
- curl -sf -m 5 \\
200
- -X POST "$OVERLORD_URL/api/protocol/permission-request?ticketId=$TICKET_ID" \\
201
- -H "Authorization: Bearer $AGENT_TOKEN" \\
202
- -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \\
203
- -H "Content-Type: application/json" \\
204
- -d "$BODY" \\
198
+ if [ -n "$TICKET_ID" ] && command -v ovld >/dev/null 2>&1; then
199
+ { if [ -n "$BODY" ]; then printf '%s' "$BODY"; else printf '{}'; fi; } \\
200
+ | ovld protocol permission-request --ticket-id "$TICKET_ID" --payload-file - \\
205
201
  >/dev/null 2>&1 &
206
202
  disown
207
203
  fi
@@ -823,9 +819,7 @@ function installOpenCode() {
823
819
  bash: {
824
820
  '*': 'ask',
825
821
  ...existingBash,
826
- 'ovld protocol *': 'allow',
827
- 'curl -sS -X POST *': 'allow',
828
- 'curl -s -X POST *': 'allow'
822
+ 'ovld protocol *': 'allow'
829
823
  }
830
824
  }
831
825
  });
@@ -904,8 +898,7 @@ function installCursor() {
904
898
  const mergedAllow = Array.from(
905
899
  new Set([
906
900
  ...asStringArray(permissions.allow),
907
- 'Shell(ovld protocol:*)',
908
- 'Shell(curl -sS -X POST:*)'
901
+ 'Shell(ovld protocol:*)'
909
902
  ])
910
903
  );
911
904
  writeJsonFile(paths.settingsFile, {
@@ -939,12 +932,6 @@ function installGemini() {
939
932
  'commandPrefix = "ovld protocol"',
940
933
  'decision = "allow"',
941
934
  'priority = 900',
942
- '',
943
- '[[rule]]',
944
- 'toolName = "run_shell_command"',
945
- 'commandPrefix = "curl -sS -X POST"',
946
- 'decision = "allow"',
947
- 'priority = 900',
948
935
  ''
949
936
  ].join('\n');
950
937
  writeTextFile(paths.policyFile, policyContent);
@@ -1277,21 +1264,7 @@ function installClaudePermissions(platformUrl) {
1277
1264
  if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
1278
1265
  }
1279
1266
 
1280
- const PROTOCOL_ENDPOINTS = [
1281
- 'attach', 'update', 'ask', 'read-context', 'write-context', 'deliver',
1282
- 'create-ticket', 'list-tickets', 'record-change-rationales', 'spawn',
1283
- 'discover-project', 'load-context', 'artifact-upload-file', 'artifact-download-url'
1284
- ];
1285
-
1286
- const entries = [];
1287
- for (const endpoint of PROTOCOL_ENDPOINTS) {
1288
- entries.push(`Bash(curl -s -X POST "${platformUrl}/api/protocol/${endpoint}":*)`);
1289
- }
1290
- entries.push(`Bash(curl -s -H 'Authorization::*)`);
1291
- for (const endpoint of PROTOCOL_ENDPOINTS) {
1292
- entries.push(`Bash(curl -s -X POST "$OVERLORD_URL/api/protocol/${endpoint}":*)`);
1293
- }
1294
- entries.push(`Bash(curl -s -H "Authorization::*)`);
1267
+ const entries = ['Bash(ovld protocol:*)'];
1295
1268
 
1296
1269
  const existing = new Set(settings.permissions.allow);
1297
1270
  const toAdd = entries.filter((e) => !existing.has(e));
@@ -1353,9 +1326,7 @@ function installOpenCodePermissions(_platformUrl) {
1353
1326
  bash: {
1354
1327
  '*': 'ask',
1355
1328
  ...existingBash,
1356
- 'ovld protocol *': 'allow',
1357
- 'curl -sS -X POST *': 'allow',
1358
- 'curl -s -X POST *': 'allow'
1329
+ 'ovld protocol *': 'allow'
1359
1330
  }
1360
1331
  }
1361
1332
  };
@@ -1374,21 +1345,12 @@ function installOpenCodePermissions(_platformUrl) {
1374
1345
  return true;
1375
1346
  }
1376
1347
 
1377
- function installCodexPermissions(platformUrl) {
1348
+ function installCodexPermissions() {
1378
1349
  console.log(`--- Codex ---`);
1379
1350
  console.log(' Codex does not support file-based permission configuration.');
1380
- console.log(' To warm up permissions, run the following commands once inside a Codex session:');
1381
- console.log(' (Codex will prompt for approval; approve each one to persist the prefix.)\n');
1382
-
1383
- const PROTOCOL_ENDPOINTS = [
1384
- 'attach', 'update', 'ask', 'read-context', 'write-context', 'deliver',
1385
- 'create-ticket', 'list-tickets'
1386
- ];
1387
-
1388
- for (const endpoint of PROTOCOL_ENDPOINTS) {
1389
- console.log(` curl -s -X POST "${platformUrl}/api/protocol/${endpoint}" -H "Content-Type: application/json" -H "Authorization: Bearer \\$AGENT_TOKEN" -d '{}'`);
1390
- }
1391
- console.log(` curl -s -H "Authorization: Bearer \\$AGENT_TOKEN" "${platformUrl}/api/protocol/context/test"`);
1351
+ console.log(' To warm up permissions, run the following command once inside a Codex session:');
1352
+ console.log(' (Codex will prompt for approval; approve it to persist the prefix.)\n');
1353
+ console.log(' ovld protocol help');
1392
1354
  console.log();
1393
1355
  return true;
1394
1356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,8 +6,8 @@ Claude Code plugin that exposes the Overlord local ticket workflow to any Claude
6
6
 
7
7
  - `skills/overlord-ticket/SKILL.md` — durable attach → update → ask → deliver workflow.
8
8
  - `commands/{connect,load,spawn}.md` — slash commands for session routing and ticket creation.
9
- - `hooks/hooks.json` + `scripts/permission-hook.sh` — PermissionRequest notifier that posts to `/api/protocol/permission-request` on the Overlord platform.
10
- - `userConfig` for `overlord_url` (non-sensitive) and `agent_token` (sensitive OS keychain) so the hook and CLI know where to talk.
9
+ - `hooks/hooks.json` + `scripts/permission-hook.sh` — PermissionRequest notifier that calls `ovld protocol permission-request`.
10
+ - `userConfig` for legacy `overlord_url` and `agent_token` installs. Current installs should authenticate with `ovld auth login` or Overlord Desktop; env vars remain optional overrides for remote shells, CI, and explicit token injection.
11
11
 
12
12
  ## Requirements
13
13
 
@@ -29,7 +29,7 @@ claude plugin marketplace add cooperativ/overlord-marketplace
29
29
  claude plugin install overlord@cooperativ
30
30
  ```
31
31
 
32
- The plugin prompts for `overlord_url` and `agent_token` at install time. The token is persisted to the OS keychain; the URL is stored in `~/.claude/settings.json` under `pluginConfigs["overlord"].options`.
32
+ Older plugin versions prompted for `overlord_url` and `agent_token` at install time. The current hook goes through `ovld protocol`, so the CLI resolves auth from env vars or the shared `~/.ovld` credentials written by CLI/Desktop login.
33
33
 
34
34
  ## Namespaced components
35
35
 
@@ -14,7 +14,7 @@ personal marketplace entry at `~/.agents/plugins/marketplace.json`.
14
14
  ## Requirements
15
15
 
16
16
  - Install the Overlord CLI so `ovld` is available on `PATH`.
17
- - Ensure `OVERLORD_URL` and `AGENT_TOKEN` are available when the target Overlord instance requires them.
17
+ - Authenticate with `ovld auth login` or Overlord Desktop. `OVERLORD_URL` and `AGENT_TOKEN` are optional overrides, mainly for remote shells, CI, or explicit token injection.
18
18
  - Optionally set `OVLD_BIN` if the CLI lives at a non-standard path.
19
19
 
20
20
  ## Tool coverage
@@ -51,16 +51,20 @@ const tools = [
51
51
  properties: {
52
52
  ticket_id: { type: 'string', description: 'Target ticket ID' },
53
53
  agent: { type: 'string' },
54
+ model: { type: 'string' },
54
55
  method: { type: 'string' },
55
- external_session_id: { type: ['string', 'null'] }
56
+ external_session_id: { type: ['string', 'null'] },
57
+ metadata: { type: 'object' }
56
58
  },
57
59
  required: ['ticket_id']
58
60
  },
59
61
  toCliFlags: args => ({
60
62
  'ticket-id': args.ticket_id,
61
63
  agent: args.agent,
64
+ model: args.model,
62
65
  method: args.method,
63
- 'external-session-id': args.external_session_id
66
+ 'external-session-id': args.external_session_id,
67
+ 'metadata-json': args.metadata
64
68
  }),
65
69
  subcommand: 'attach'
66
70
  },
@@ -116,6 +120,8 @@ const tools = [
116
120
  parent_session_key: { type: 'string' },
117
121
  parent_ticket_id: { type: 'string' },
118
122
  agent: { type: 'string' },
123
+ model: { type: 'string' },
124
+ metadata: { type: 'object' },
119
125
  method: { type: 'string' }
120
126
  },
121
127
  required: ['objective']
@@ -133,6 +139,8 @@ const tools = [
133
139
  'parent-session-key': args.parent_session_key,
134
140
  'parent-ticket-id': args.parent_ticket_id,
135
141
  agent: args.agent,
142
+ model: args.model,
143
+ 'metadata-json': args.metadata,
136
144
  method: args.method
137
145
  }),
138
146
  subcommand: 'spawn'