open-research-protocol 0.4.27 → 0.4.29
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/CHANGELOG.md +48 -0
- package/README.md +39 -14
- package/bin/orp.js +14 -1
- package/cli/__pycache__/orp.cpython-311.pyc +0 -0
- package/cli/orp.py +1124 -5
- package/docs/START_HERE.md +14 -0
- package/package.json +5 -1
- package/packages/orp-workspace-launcher/src/codex.js +822 -0
- package/packages/orp-workspace-launcher/src/index.js +10 -0
- package/packages/orp-workspace-launcher/src/ledger.js +11 -1
- package/packages/orp-workspace-launcher/test/codex.test.js +309 -0
- package/scripts/__pycache__/orp-kernel-agent-pilot.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-replication.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-benchmark.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-canonical-continuation.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-continuation-pilot.cpython-311.pyc +0 -0
- package/scripts/render-terminal-demo.py +262 -134
package/cli/orp.py
CHANGED
|
@@ -141,6 +141,8 @@ FRONTIER_TERMINAL_STATUSES = {"complete", "completed", "done", "skipped", "termi
|
|
|
141
141
|
YOUTUBE_SOURCE_SCHEMA_VERSION = "1.0.0"
|
|
142
142
|
EXCHANGE_REPORT_SCHEMA_VERSION = "1.0.0"
|
|
143
143
|
RESEARCH_RUN_SCHEMA_VERSION = "1.0.0"
|
|
144
|
+
SECRET_SPEND_POLICY_SCHEMA_VERSION = "1.0.0"
|
|
145
|
+
RESEARCH_SPEND_LEDGER_SCHEMA_VERSION = "1.0.0"
|
|
144
146
|
PROJECT_CONTEXT_SCHEMA_VERSION = "1.0.0"
|
|
145
147
|
HYGIENE_POLICY_SCHEMA_VERSION = "1.0.0"
|
|
146
148
|
MAINTENANCE_STATE_SCHEMA_VERSION = "1.0.0"
|
|
@@ -922,6 +924,45 @@ def _keychain_secret_registry_path() -> Path:
|
|
|
922
924
|
return _orp_user_dir() / "secrets-keychain.json"
|
|
923
925
|
|
|
924
926
|
|
|
927
|
+
def _research_spend_ledger_path() -> Path:
|
|
928
|
+
return _orp_user_dir() / "research-spend-ledger.json"
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _research_spend_ledger_template() -> dict[str, Any]:
|
|
932
|
+
return {
|
|
933
|
+
"schema_version": RESEARCH_SPEND_LEDGER_SCHEMA_VERSION,
|
|
934
|
+
"records": [],
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _load_research_spend_ledger() -> dict[str, Any]:
|
|
939
|
+
path = _research_spend_ledger_path()
|
|
940
|
+
if not path.exists():
|
|
941
|
+
return _research_spend_ledger_template()
|
|
942
|
+
try:
|
|
943
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
944
|
+
except Exception:
|
|
945
|
+
return _research_spend_ledger_template()
|
|
946
|
+
if not isinstance(payload, dict):
|
|
947
|
+
return _research_spend_ledger_template()
|
|
948
|
+
records = payload.get("records")
|
|
949
|
+
return {
|
|
950
|
+
"schema_version": str(payload.get("schema_version", RESEARCH_SPEND_LEDGER_SCHEMA_VERSION)).strip()
|
|
951
|
+
or RESEARCH_SPEND_LEDGER_SCHEMA_VERSION,
|
|
952
|
+
"records": [row for row in records if isinstance(row, dict)] if isinstance(records, list) else [],
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _save_research_spend_ledger(ledger: dict[str, Any]) -> None:
|
|
957
|
+
records = ledger.get("records")
|
|
958
|
+
payload = {
|
|
959
|
+
"schema_version": str(ledger.get("schema_version", RESEARCH_SPEND_LEDGER_SCHEMA_VERSION)).strip()
|
|
960
|
+
or RESEARCH_SPEND_LEDGER_SCHEMA_VERSION,
|
|
961
|
+
"records": [row for row in records if isinstance(row, dict)] if isinstance(records, list) else [],
|
|
962
|
+
}
|
|
963
|
+
_write_json(_research_spend_ledger_path(), payload)
|
|
964
|
+
|
|
965
|
+
|
|
925
966
|
def _keychain_supported() -> bool:
|
|
926
967
|
return sys.platform == "darwin" or os.environ.get("ORP_KEYCHAIN_ALLOW_NON_DARWIN", "").strip() == "1"
|
|
927
968
|
|
|
@@ -10046,6 +10087,462 @@ def _effective_remote_context(
|
|
|
10046
10087
|
}
|
|
10047
10088
|
|
|
10048
10089
|
|
|
10090
|
+
def _command_preview(args: Sequence[str]) -> str:
|
|
10091
|
+
return " ".join(shlex.quote(str(arg)) for arg in args)
|
|
10092
|
+
|
|
10093
|
+
|
|
10094
|
+
def _tool_path(tool_name: str) -> str:
|
|
10095
|
+
return shutil.which(str(tool_name or "").strip()) or ""
|
|
10096
|
+
|
|
10097
|
+
|
|
10098
|
+
def _run_checked_process(
|
|
10099
|
+
args: Sequence[str],
|
|
10100
|
+
*,
|
|
10101
|
+
cwd: Path,
|
|
10102
|
+
context: str,
|
|
10103
|
+
) -> subprocess.CompletedProcess[str]:
|
|
10104
|
+
command = [str(arg) for arg in args]
|
|
10105
|
+
try:
|
|
10106
|
+
proc = subprocess.run(
|
|
10107
|
+
command,
|
|
10108
|
+
cwd=str(cwd),
|
|
10109
|
+
capture_output=True,
|
|
10110
|
+
text=True,
|
|
10111
|
+
)
|
|
10112
|
+
except FileNotFoundError as exc:
|
|
10113
|
+
raise RuntimeError(f"{context} requires `{command[0]}` on PATH.") from exc
|
|
10114
|
+
if proc.returncode != 0:
|
|
10115
|
+
detail = proc.stderr.strip() or proc.stdout.strip() or f"exit code {proc.returncode}"
|
|
10116
|
+
raise RuntimeError(f"{context} failed: {detail}")
|
|
10117
|
+
return proc
|
|
10118
|
+
|
|
10119
|
+
|
|
10120
|
+
def _run_optional_process(
|
|
10121
|
+
args: Sequence[str],
|
|
10122
|
+
*,
|
|
10123
|
+
cwd: Path,
|
|
10124
|
+
) -> dict[str, Any]:
|
|
10125
|
+
command = [str(arg) for arg in args]
|
|
10126
|
+
try:
|
|
10127
|
+
proc = subprocess.run(
|
|
10128
|
+
command,
|
|
10129
|
+
cwd=str(cwd),
|
|
10130
|
+
capture_output=True,
|
|
10131
|
+
text=True,
|
|
10132
|
+
)
|
|
10133
|
+
except FileNotFoundError as exc:
|
|
10134
|
+
return {
|
|
10135
|
+
"ok": False,
|
|
10136
|
+
"command": _command_preview(command),
|
|
10137
|
+
"returncode": 127,
|
|
10138
|
+
"detail": str(exc),
|
|
10139
|
+
}
|
|
10140
|
+
detail = proc.stderr.strip() or proc.stdout.strip()
|
|
10141
|
+
return {
|
|
10142
|
+
"ok": proc.returncode == 0,
|
|
10143
|
+
"command": _command_preview(command),
|
|
10144
|
+
"returncode": int(proc.returncode),
|
|
10145
|
+
"detail": _truncate(detail, limit=600) if detail else "",
|
|
10146
|
+
}
|
|
10147
|
+
|
|
10148
|
+
|
|
10149
|
+
def _init_startup_enabled(args: argparse.Namespace) -> bool:
|
|
10150
|
+
bool_flags = [
|
|
10151
|
+
"project_startup",
|
|
10152
|
+
"private_github",
|
|
10153
|
+
"track_workspace_main",
|
|
10154
|
+
"with_clawdad",
|
|
10155
|
+
"current_codex",
|
|
10156
|
+
"workspace_append",
|
|
10157
|
+
]
|
|
10158
|
+
text_flags = [
|
|
10159
|
+
"codex_session_id",
|
|
10160
|
+
"workspace_title",
|
|
10161
|
+
"workspace_bootstrap_command",
|
|
10162
|
+
"workspace_name",
|
|
10163
|
+
"clawdad_slug",
|
|
10164
|
+
"clawdad_description",
|
|
10165
|
+
]
|
|
10166
|
+
if any(bool(getattr(args, name, False)) for name in bool_flags):
|
|
10167
|
+
return True
|
|
10168
|
+
return any(str(getattr(args, name, "") or "").strip() for name in text_flags if name != "workspace_name")
|
|
10169
|
+
|
|
10170
|
+
|
|
10171
|
+
def _init_startup_context(args: argparse.Namespace) -> dict[str, Any]:
|
|
10172
|
+
enabled = _init_startup_enabled(args)
|
|
10173
|
+
project_startup = bool(getattr(args, "project_startup", False))
|
|
10174
|
+
github_repo = str(getattr(args, "github_repo", "") or "").strip()
|
|
10175
|
+
clawdad_path = _tool_path("clawdad")
|
|
10176
|
+
return {
|
|
10177
|
+
"enabled": enabled,
|
|
10178
|
+
"project_startup": project_startup,
|
|
10179
|
+
"dry_run": bool(getattr(args, "startup_dry_run", False)),
|
|
10180
|
+
"tools": {
|
|
10181
|
+
"gh": _tool_path("gh"),
|
|
10182
|
+
"clawdad": clawdad_path,
|
|
10183
|
+
"orp": _tool_path("orp"),
|
|
10184
|
+
},
|
|
10185
|
+
"github": {
|
|
10186
|
+
"requested": bool(getattr(args, "private_github", False)) or bool(project_startup and github_repo),
|
|
10187
|
+
"repo": "",
|
|
10188
|
+
"remote_url": "",
|
|
10189
|
+
"action": "not_requested",
|
|
10190
|
+
},
|
|
10191
|
+
"workspace": {
|
|
10192
|
+
"requested": bool(
|
|
10193
|
+
getattr(args, "track_workspace_main", False)
|
|
10194
|
+
or project_startup
|
|
10195
|
+
or getattr(args, "current_codex", False)
|
|
10196
|
+
or str(getattr(args, "codex_session_id", "") or "").strip()
|
|
10197
|
+
or str(getattr(args, "workspace_title", "") or "").strip()
|
|
10198
|
+
or str(getattr(args, "workspace_bootstrap_command", "") or "").strip()
|
|
10199
|
+
),
|
|
10200
|
+
"workspace": str(getattr(args, "workspace_name", "") or "main").strip() or "main",
|
|
10201
|
+
"action": "not_requested",
|
|
10202
|
+
},
|
|
10203
|
+
"clawdad": {
|
|
10204
|
+
"requested": bool(getattr(args, "with_clawdad", False)) or bool(project_startup and clawdad_path),
|
|
10205
|
+
"available": bool(clawdad_path),
|
|
10206
|
+
"action": "not_requested",
|
|
10207
|
+
},
|
|
10208
|
+
"codex": {
|
|
10209
|
+
"requested_current": bool(getattr(args, "current_codex", False)),
|
|
10210
|
+
"session_id": "",
|
|
10211
|
+
"source": "",
|
|
10212
|
+
},
|
|
10213
|
+
"commands": [],
|
|
10214
|
+
"warnings": [],
|
|
10215
|
+
"next_actions": [],
|
|
10216
|
+
"ok": True,
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
|
|
10220
|
+
def _startup_codex_context(args: argparse.Namespace, startup: dict[str, Any]) -> dict[str, str]:
|
|
10221
|
+
explicit_session = str(getattr(args, "codex_session_id", "") or "").strip()
|
|
10222
|
+
env_session = str(os.environ.get("CODEX_THREAD_ID", "") or "").strip()
|
|
10223
|
+
requested_current = bool(getattr(args, "current_codex", False))
|
|
10224
|
+
session_id = explicit_session or (env_session if requested_current else "")
|
|
10225
|
+
source = "explicit" if explicit_session else ("CODEX_THREAD_ID" if session_id else "")
|
|
10226
|
+
if explicit_session and requested_current and env_session and explicit_session != env_session:
|
|
10227
|
+
startup["warnings"].append(
|
|
10228
|
+
"both --codex-session-id and --current-codex were provided; using the explicit session id."
|
|
10229
|
+
)
|
|
10230
|
+
if requested_current and not session_id:
|
|
10231
|
+
startup["warnings"].append("CODEX_THREAD_ID is not set; workspace path will be tracked without a Codex resume target.")
|
|
10232
|
+
startup["next_actions"].append("rerun from an active Codex session with --current-codex, or pass --codex-session-id <id>")
|
|
10233
|
+
startup["codex"] = {
|
|
10234
|
+
"requested_current": requested_current,
|
|
10235
|
+
"session_id": session_id,
|
|
10236
|
+
"source": source,
|
|
10237
|
+
}
|
|
10238
|
+
return {"session_id": session_id, "source": source}
|
|
10239
|
+
|
|
10240
|
+
|
|
10241
|
+
def _setup_private_github_startup_remote(
|
|
10242
|
+
*,
|
|
10243
|
+
repo_root: Path,
|
|
10244
|
+
github_repo_raw: str,
|
|
10245
|
+
dry_run: bool,
|
|
10246
|
+
) -> dict[str, Any]:
|
|
10247
|
+
github_repo = _normalize_github_repo(github_repo_raw)
|
|
10248
|
+
if not github_repo:
|
|
10249
|
+
raise RuntimeError("--private-github requires --github-repo owner/repo.")
|
|
10250
|
+
remote_url = _synthesized_github_remote_url(github_repo)
|
|
10251
|
+
existing_origin = _git_stdout(repo_root, ["remote", "get-url", "origin"])
|
|
10252
|
+
payload: dict[str, Any] = {
|
|
10253
|
+
"requested": True,
|
|
10254
|
+
"repo": github_repo,
|
|
10255
|
+
"remote_url": remote_url,
|
|
10256
|
+
"existing_origin": existing_origin,
|
|
10257
|
+
"action": "",
|
|
10258
|
+
"command": "",
|
|
10259
|
+
}
|
|
10260
|
+
if existing_origin:
|
|
10261
|
+
existing_repo = _github_repo_from_remote_url(existing_origin)
|
|
10262
|
+
if existing_repo and existing_repo != github_repo:
|
|
10263
|
+
raise RuntimeError(
|
|
10264
|
+
f"origin already points to `{existing_origin}`, not GitHub repo `{github_repo}`."
|
|
10265
|
+
)
|
|
10266
|
+
payload["action"] = "kept"
|
|
10267
|
+
payload["remote_url"] = existing_origin
|
|
10268
|
+
return payload
|
|
10269
|
+
|
|
10270
|
+
command = [
|
|
10271
|
+
"gh",
|
|
10272
|
+
"repo",
|
|
10273
|
+
"create",
|
|
10274
|
+
github_repo,
|
|
10275
|
+
"--private",
|
|
10276
|
+
"--source",
|
|
10277
|
+
str(repo_root),
|
|
10278
|
+
"--remote",
|
|
10279
|
+
"origin",
|
|
10280
|
+
]
|
|
10281
|
+
payload["command"] = _command_preview(command)
|
|
10282
|
+
if dry_run:
|
|
10283
|
+
payload["action"] = "planned"
|
|
10284
|
+
return payload
|
|
10285
|
+
|
|
10286
|
+
gh_path = _tool_path("gh")
|
|
10287
|
+
if not gh_path:
|
|
10288
|
+
raise RuntimeError("creating a private GitHub remote requires the `gh` CLI on PATH.")
|
|
10289
|
+
_run_checked_process(
|
|
10290
|
+
[gh_path, *command[1:]],
|
|
10291
|
+
cwd=repo_root,
|
|
10292
|
+
context="private GitHub remote setup",
|
|
10293
|
+
)
|
|
10294
|
+
detected_origin = _git_stdout(repo_root, ["remote", "get-url", "origin"])
|
|
10295
|
+
payload["action"] = "created"
|
|
10296
|
+
payload["remote_url"] = detected_origin or remote_url
|
|
10297
|
+
payload["detected_origin_after"] = detected_origin
|
|
10298
|
+
return payload
|
|
10299
|
+
|
|
10300
|
+
|
|
10301
|
+
def _orp_workspace_cli_prefix() -> list[str]:
|
|
10302
|
+
override = str(os.environ.get("ORP_CLI", "") or "").strip()
|
|
10303
|
+
if override:
|
|
10304
|
+
return shlex.split(override)
|
|
10305
|
+
local_bin = _orp_repo_root() / "bin" / "orp.js"
|
|
10306
|
+
if local_bin.exists() and os.access(local_bin, os.X_OK):
|
|
10307
|
+
return [str(local_bin)]
|
|
10308
|
+
node_path = _tool_path("node")
|
|
10309
|
+
if local_bin.exists() and node_path:
|
|
10310
|
+
return [node_path, str(local_bin)]
|
|
10311
|
+
orp_path = _tool_path("orp")
|
|
10312
|
+
if orp_path:
|
|
10313
|
+
return [orp_path]
|
|
10314
|
+
return ["orp"]
|
|
10315
|
+
|
|
10316
|
+
|
|
10317
|
+
def _setup_workspace_startup_tracking(
|
|
10318
|
+
*,
|
|
10319
|
+
repo_root: Path,
|
|
10320
|
+
args: argparse.Namespace,
|
|
10321
|
+
default_branch: str,
|
|
10322
|
+
remote_url: str,
|
|
10323
|
+
codex_session_id: str,
|
|
10324
|
+
dry_run: bool,
|
|
10325
|
+
) -> dict[str, Any]:
|
|
10326
|
+
workspace_name = str(getattr(args, "workspace_name", "") or "main").strip() or "main"
|
|
10327
|
+
command = [
|
|
10328
|
+
*_orp_workspace_cli_prefix(),
|
|
10329
|
+
"workspace",
|
|
10330
|
+
"add-tab",
|
|
10331
|
+
workspace_name,
|
|
10332
|
+
"--path",
|
|
10333
|
+
str(repo_root),
|
|
10334
|
+
]
|
|
10335
|
+
title = str(getattr(args, "workspace_title", "") or "").strip()
|
|
10336
|
+
if title:
|
|
10337
|
+
command.extend(["--title", title])
|
|
10338
|
+
if remote_url:
|
|
10339
|
+
command.extend(["--remote-url", remote_url, "--remote-branch", default_branch])
|
|
10340
|
+
bootstrap_command = str(getattr(args, "workspace_bootstrap_command", "") or "").strip()
|
|
10341
|
+
if bootstrap_command:
|
|
10342
|
+
command.extend(["--bootstrap-command", bootstrap_command])
|
|
10343
|
+
if codex_session_id:
|
|
10344
|
+
command.extend(["--resume-tool", "codex", "--resume-session-id", codex_session_id])
|
|
10345
|
+
elif bool(getattr(args, "current_codex", False)) and os.environ.get("CODEX_THREAD_ID"):
|
|
10346
|
+
command.append("--current-codex")
|
|
10347
|
+
if bool(getattr(args, "workspace_append", False)):
|
|
10348
|
+
command.append("--append")
|
|
10349
|
+
command.append("--json")
|
|
10350
|
+
payload: dict[str, Any] = {
|
|
10351
|
+
"requested": True,
|
|
10352
|
+
"workspace": workspace_name,
|
|
10353
|
+
"path": str(repo_root),
|
|
10354
|
+
"remote_url": remote_url,
|
|
10355
|
+
"remote_branch": default_branch if remote_url else "",
|
|
10356
|
+
"codex_session_id": codex_session_id,
|
|
10357
|
+
"action": "planned" if dry_run else "",
|
|
10358
|
+
"command": _command_preview(command),
|
|
10359
|
+
}
|
|
10360
|
+
if dry_run:
|
|
10361
|
+
return payload
|
|
10362
|
+
|
|
10363
|
+
proc = _run_checked_process(command, cwd=repo_root, context="workspace main tracking")
|
|
10364
|
+
payload["action"] = "updated"
|
|
10365
|
+
try:
|
|
10366
|
+
payload["result"] = json.loads(proc.stdout)
|
|
10367
|
+
except Exception:
|
|
10368
|
+
payload["result"] = {"stdout": _truncate(proc.stdout.strip(), limit=600)}
|
|
10369
|
+
return payload
|
|
10370
|
+
|
|
10371
|
+
|
|
10372
|
+
def _clawdad_delegate_brief_template(
|
|
10373
|
+
*,
|
|
10374
|
+
repo_root: Path,
|
|
10375
|
+
remote_url: str,
|
|
10376
|
+
workspace_name: str,
|
|
10377
|
+
) -> str:
|
|
10378
|
+
return (
|
|
10379
|
+
"# Clawdad Delegate Brief\n\n"
|
|
10380
|
+
f"- Project: `{repo_root.name}`\n"
|
|
10381
|
+
f"- Root: `{repo_root}`\n"
|
|
10382
|
+
f"- Remote: `{remote_url or '(local only)'}`\n"
|
|
10383
|
+
f"- ORP workspace: `{workspace_name}`\n\n"
|
|
10384
|
+
"## Startup Contract\n\n"
|
|
10385
|
+
"- This project is ORP-governed and registered for Clawdad/Codex delegation.\n"
|
|
10386
|
+
"- Run `orp status --json` and `orp hygiene --json` before long-running expansion.\n"
|
|
10387
|
+
"- Stop when hygiene reports `dirty_unclassified`; classify, refresh, canonicalize, or write a blocker.\n"
|
|
10388
|
+
"- Do not reset, checkout, or delete files merely to hide dirty state.\n"
|
|
10389
|
+
"- Keep canonical project state in repo files and keep process state in ORP/Clawdad ledgers.\n\n"
|
|
10390
|
+
"## First Checks\n\n"
|
|
10391
|
+
f"- `orp workspace tabs {workspace_name}`\n"
|
|
10392
|
+
"- `orp project show --json`\n"
|
|
10393
|
+
"- `orp hygiene --json`\n"
|
|
10394
|
+
"- `clawdad delegate <project>`\n\n"
|
|
10395
|
+
"## Delegate Posture\n\n"
|
|
10396
|
+
"- Prefer bounded, concrete tasks with a clear write scope.\n"
|
|
10397
|
+
"- Refresh project context after meaningful docs, manifest, roadmap, or agent-guidance changes.\n"
|
|
10398
|
+
"- Write a blocker instead of forcing progress when the repo state is ambiguous.\n"
|
|
10399
|
+
)
|
|
10400
|
+
|
|
10401
|
+
|
|
10402
|
+
def _setup_clawdad_startup(
|
|
10403
|
+
*,
|
|
10404
|
+
repo_root: Path,
|
|
10405
|
+
args: argparse.Namespace,
|
|
10406
|
+
remote_url: str,
|
|
10407
|
+
codex_session_id: str,
|
|
10408
|
+
dry_run: bool,
|
|
10409
|
+
) -> dict[str, Any]:
|
|
10410
|
+
workspace_name = str(getattr(args, "workspace_name", "") or "main").strip() or "main"
|
|
10411
|
+
brief_path = repo_root / "orp" / "clawdad" / "DELEGATE_BRIEF.md"
|
|
10412
|
+
brief_action = _write_text_if_missing(
|
|
10413
|
+
brief_path,
|
|
10414
|
+
_clawdad_delegate_brief_template(
|
|
10415
|
+
repo_root=repo_root,
|
|
10416
|
+
remote_url=remote_url,
|
|
10417
|
+
workspace_name=workspace_name,
|
|
10418
|
+
),
|
|
10419
|
+
)
|
|
10420
|
+
clawdad_path = _tool_path("clawdad")
|
|
10421
|
+
payload: dict[str, Any] = {
|
|
10422
|
+
"requested": True,
|
|
10423
|
+
"available": bool(clawdad_path),
|
|
10424
|
+
"brief_path": _path_for_state(brief_path, repo_root),
|
|
10425
|
+
"brief_action": brief_action,
|
|
10426
|
+
"action": "planned" if dry_run else "",
|
|
10427
|
+
"commands": [],
|
|
10428
|
+
"results": [],
|
|
10429
|
+
"ok": True,
|
|
10430
|
+
}
|
|
10431
|
+
description = str(getattr(args, "clawdad_description", "") or "").strip()
|
|
10432
|
+
if not description:
|
|
10433
|
+
description = f"ORP-governed project: {repo_root.name}"
|
|
10434
|
+
register_command = [
|
|
10435
|
+
"clawdad",
|
|
10436
|
+
"register",
|
|
10437
|
+
str(repo_root),
|
|
10438
|
+
"--provider",
|
|
10439
|
+
"codex",
|
|
10440
|
+
"--description",
|
|
10441
|
+
description,
|
|
10442
|
+
]
|
|
10443
|
+
slug = str(getattr(args, "clawdad_slug", "") or "").strip()
|
|
10444
|
+
if slug:
|
|
10445
|
+
register_command.extend(["--slug", slug])
|
|
10446
|
+
commands = [
|
|
10447
|
+
register_command,
|
|
10448
|
+
["clawdad", "delegate-set", str(repo_root), "--file", str(brief_path)],
|
|
10449
|
+
]
|
|
10450
|
+
if codex_session_id:
|
|
10451
|
+
commands.append(["clawdad", "track-session", str(repo_root), codex_session_id])
|
|
10452
|
+
payload["commands"] = [_command_preview(command) for command in commands]
|
|
10453
|
+
if not clawdad_path:
|
|
10454
|
+
payload["action"] = "skipped_missing_clawdad"
|
|
10455
|
+
payload["ok"] = False
|
|
10456
|
+
return payload
|
|
10457
|
+
if dry_run:
|
|
10458
|
+
return payload
|
|
10459
|
+
|
|
10460
|
+
results: list[dict[str, Any]] = []
|
|
10461
|
+
for command in commands:
|
|
10462
|
+
result = _run_optional_process([clawdad_path, *command[1:]], cwd=repo_root)
|
|
10463
|
+
result["command"] = _command_preview(command)
|
|
10464
|
+
results.append(result)
|
|
10465
|
+
if not result.get("ok"):
|
|
10466
|
+
payload["ok"] = False
|
|
10467
|
+
break
|
|
10468
|
+
payload["results"] = results
|
|
10469
|
+
payload["action"] = "updated" if payload["ok"] else "blocked"
|
|
10470
|
+
return payload
|
|
10471
|
+
|
|
10472
|
+
|
|
10473
|
+
def _finish_init_startup(
|
|
10474
|
+
*,
|
|
10475
|
+
repo_root: Path,
|
|
10476
|
+
args: argparse.Namespace,
|
|
10477
|
+
default_branch: str,
|
|
10478
|
+
remote_context: dict[str, Any],
|
|
10479
|
+
startup: dict[str, Any],
|
|
10480
|
+
files: dict[str, dict[str, str]],
|
|
10481
|
+
notes: list[str],
|
|
10482
|
+
warnings: list[str],
|
|
10483
|
+
next_actions: list[str],
|
|
10484
|
+
) -> dict[str, Any]:
|
|
10485
|
+
if not startup.get("enabled"):
|
|
10486
|
+
return startup
|
|
10487
|
+
dry_run = bool(startup.get("dry_run"))
|
|
10488
|
+
codex = _startup_codex_context(args, startup)
|
|
10489
|
+
actual_origin = _git_stdout(repo_root, ["remote", "get-url", "origin"])
|
|
10490
|
+
remote_url = actual_origin or str(remote_context.get("effective_remote_url", "") or "").strip()
|
|
10491
|
+
|
|
10492
|
+
if bool(getattr(args, "project_startup", False)) and not startup.get("github", {}).get("requested"):
|
|
10493
|
+
startup["next_actions"].append("pass --github-repo owner/repo to let project startup create or record a private GitHub remote")
|
|
10494
|
+
|
|
10495
|
+
if startup.get("workspace", {}).get("requested"):
|
|
10496
|
+
startup["workspace"] = _setup_workspace_startup_tracking(
|
|
10497
|
+
repo_root=repo_root,
|
|
10498
|
+
args=args,
|
|
10499
|
+
default_branch=default_branch,
|
|
10500
|
+
remote_url=remote_url,
|
|
10501
|
+
codex_session_id=codex["session_id"],
|
|
10502
|
+
dry_run=dry_run,
|
|
10503
|
+
)
|
|
10504
|
+
startup["commands"].append(startup["workspace"].get("command", ""))
|
|
10505
|
+
|
|
10506
|
+
if startup.get("clawdad", {}).get("requested"):
|
|
10507
|
+
startup["clawdad"] = _setup_clawdad_startup(
|
|
10508
|
+
repo_root=repo_root,
|
|
10509
|
+
args=args,
|
|
10510
|
+
remote_url=remote_url,
|
|
10511
|
+
codex_session_id=codex["session_id"],
|
|
10512
|
+
dry_run=dry_run,
|
|
10513
|
+
)
|
|
10514
|
+
files["clawdad_delegate_brief"] = {
|
|
10515
|
+
"path": str(startup["clawdad"].get("brief_path", "")),
|
|
10516
|
+
"action": str(startup["clawdad"].get("brief_action", "")),
|
|
10517
|
+
}
|
|
10518
|
+
startup["commands"].extend(startup["clawdad"].get("commands", []))
|
|
10519
|
+
if startup["clawdad"].get("action") == "skipped_missing_clawdad":
|
|
10520
|
+
startup["warnings"].append("Clawdad is not installed on PATH; ORP wrote the delegate brief but did not register the project.")
|
|
10521
|
+
startup["next_actions"].append("install/run `clawdad init`, then rerun `orp init --with-clawdad`")
|
|
10522
|
+
elif startup["clawdad"].get("ok") is False:
|
|
10523
|
+
startup["warnings"].append("Clawdad registration did not complete; inspect startup.clawdad.results for the command output.")
|
|
10524
|
+
|
|
10525
|
+
if bool(getattr(args, "project_startup", False)) and not _tool_path("clawdad"):
|
|
10526
|
+
startup["notes"] = [
|
|
10527
|
+
"project startup did not enable Clawdad delegation because `clawdad` was not found on PATH."
|
|
10528
|
+
]
|
|
10529
|
+
else:
|
|
10530
|
+
startup["notes"] = []
|
|
10531
|
+
|
|
10532
|
+
startup["warnings"] = _unique_strings([str(item) for item in startup.get("warnings", []) if str(item).strip()])
|
|
10533
|
+
startup["next_actions"] = _unique_strings(
|
|
10534
|
+
[str(item) for item in startup.get("next_actions", []) if str(item).strip()]
|
|
10535
|
+
)
|
|
10536
|
+
warnings.extend(startup["warnings"])
|
|
10537
|
+
next_actions.extend(startup["next_actions"])
|
|
10538
|
+
notes.extend(startup.get("notes", []))
|
|
10539
|
+
|
|
10540
|
+
startup["ok"] = bool(startup.get("ok", True)) and bool(startup.get("workspace", {}).get("action") != "blocked")
|
|
10541
|
+
if startup.get("clawdad", {}).get("requested") and startup.get("clawdad", {}).get("ok") is False:
|
|
10542
|
+
startup["ok"] = False if bool(getattr(args, "with_clawdad", False)) else bool(startup["ok"])
|
|
10543
|
+
return startup
|
|
10544
|
+
|
|
10545
|
+
|
|
10049
10546
|
def _project_context_path(repo_root: Path) -> Path:
|
|
10050
10547
|
return repo_root / "orp" / "project.json"
|
|
10051
10548
|
|
|
@@ -12542,6 +13039,10 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
12542
13039
|
"label": "Classify dirty worktree paths before long agent expansion",
|
|
12543
13040
|
"command": "orp hygiene --json",
|
|
12544
13041
|
},
|
|
13042
|
+
{
|
|
13043
|
+
"label": "Bootstrap a new project with private GitHub, workspace main, and Clawdad when installed",
|
|
13044
|
+
"command": "orp init --project-startup --github-repo owner/repo --current-codex",
|
|
13045
|
+
},
|
|
12545
13046
|
{
|
|
12546
13047
|
"label": "Inspect the saved service and data connections for this user",
|
|
12547
13048
|
"command": "orp connections list",
|
|
@@ -13561,6 +14062,22 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
13561
14062
|
if not git_was_present:
|
|
13562
14063
|
git_init_result = _git_init_repo(repo_root, default_branch)
|
|
13563
14064
|
|
|
14065
|
+
startup = _init_startup_context(args)
|
|
14066
|
+
if startup.get("enabled") and startup.get("github", {}).get("requested"):
|
|
14067
|
+
_effective_remote_context(
|
|
14068
|
+
detected_remote_url="",
|
|
14069
|
+
detected_github_repo="",
|
|
14070
|
+
remote_url_arg=str(getattr(args, "remote_url", "") or ""),
|
|
14071
|
+
github_repo_arg=str(getattr(args, "github_repo", "") or ""),
|
|
14072
|
+
)
|
|
14073
|
+
startup["github"] = _setup_private_github_startup_remote(
|
|
14074
|
+
repo_root=repo_root,
|
|
14075
|
+
github_repo_raw=str(getattr(args, "github_repo", "") or getattr(args, "remote_url", "") or ""),
|
|
14076
|
+
dry_run=bool(startup.get("dry_run")),
|
|
14077
|
+
)
|
|
14078
|
+
if startup["github"].get("command"):
|
|
14079
|
+
startup["commands"].append(str(startup["github"]["command"]))
|
|
14080
|
+
|
|
13564
14081
|
_ensure_dirs(repo_root)
|
|
13565
14082
|
config_path = repo_root / args.config
|
|
13566
14083
|
config_action = "kept"
|
|
@@ -13746,8 +14263,34 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
13746
14263
|
"action": project_context_action,
|
|
13747
14264
|
}
|
|
13748
14265
|
|
|
14266
|
+
if startup.get("enabled"):
|
|
14267
|
+
startup = _finish_init_startup(
|
|
14268
|
+
repo_root=repo_root,
|
|
14269
|
+
args=args,
|
|
14270
|
+
default_branch=default_branch,
|
|
14271
|
+
remote_context=remote_context,
|
|
14272
|
+
startup=startup,
|
|
14273
|
+
files=files,
|
|
14274
|
+
notes=notes,
|
|
14275
|
+
warnings=warnings,
|
|
14276
|
+
next_actions=next_actions,
|
|
14277
|
+
)
|
|
14278
|
+
state = _read_json(state_path) if state_path.exists() else _default_state_payload()
|
|
14279
|
+
if not isinstance(state, dict):
|
|
14280
|
+
state = _default_state_payload()
|
|
14281
|
+
state["startup"] = {
|
|
14282
|
+
"updated_at_utc": _now_utc(),
|
|
14283
|
+
"enabled": bool(startup.get("enabled")),
|
|
14284
|
+
"project_startup": bool(startup.get("project_startup")),
|
|
14285
|
+
"github": startup.get("github", {}),
|
|
14286
|
+
"workspace": startup.get("workspace", {}),
|
|
14287
|
+
"clawdad": startup.get("clawdad", {}),
|
|
14288
|
+
"codex": startup.get("codex", {}),
|
|
14289
|
+
}
|
|
14290
|
+
_write_json(state_path, state)
|
|
14291
|
+
|
|
13749
14292
|
result = {
|
|
13750
|
-
"ok": True,
|
|
14293
|
+
"ok": bool(startup.get("ok", True)),
|
|
13751
14294
|
"config_action": config_action,
|
|
13752
14295
|
"config_path": str(config_path),
|
|
13753
14296
|
"runtime_root": str(repo_root / "orp"),
|
|
@@ -13779,6 +14322,8 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
13779
14322
|
"notes": notes,
|
|
13780
14323
|
"next_actions": next_actions,
|
|
13781
14324
|
}
|
|
14325
|
+
if startup.get("enabled"):
|
|
14326
|
+
result["startup"] = startup
|
|
13782
14327
|
if args.json_output:
|
|
13783
14328
|
_print_json(result)
|
|
13784
14329
|
else:
|
|
@@ -13792,6 +14337,11 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
13792
14337
|
print("synced AGENTS.md and CLAUDE.md with ORP-managed blocks")
|
|
13793
14338
|
print(f"project_context={_path_for_state(_project_context_path(repo_root), repo_root)}")
|
|
13794
14339
|
print(f"hygiene_policy={_path_for_state(hygiene_policy_path, repo_root)}")
|
|
14340
|
+
if startup.get("enabled"):
|
|
14341
|
+
print(f"startup.ok={'true' if startup.get('ok') else 'false'}")
|
|
14342
|
+
print(f"startup.github={startup.get('github', {}).get('action', 'not_requested')}")
|
|
14343
|
+
print(f"startup.workspace={startup.get('workspace', {}).get('action', 'not_requested')}")
|
|
14344
|
+
print(f"startup.clawdad={startup.get('clawdad', {}).get('action', 'not_requested')}")
|
|
13795
14345
|
print(
|
|
13796
14346
|
"git_state="
|
|
13797
14347
|
+ ",".join(
|
|
@@ -17166,6 +17716,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17166
17716
|
"web_search": True,
|
|
17167
17717
|
"web_search_tool": "web_search_preview",
|
|
17168
17718
|
"background": False,
|
|
17719
|
+
"spend_reserve_usd": 1.5,
|
|
17169
17720
|
"max_tool_calls": 40,
|
|
17170
17721
|
"max_output_tokens": 12000,
|
|
17171
17722
|
},
|
|
@@ -17199,6 +17750,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17199
17750
|
"secret_alias": "openai-primary",
|
|
17200
17751
|
"reasoning_effort": "high",
|
|
17201
17752
|
"text_verbosity": "medium",
|
|
17753
|
+
"spend_reserve_usd": 0.5,
|
|
17202
17754
|
"max_output_tokens": 4200,
|
|
17203
17755
|
},
|
|
17204
17756
|
{
|
|
@@ -17235,6 +17787,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17235
17787
|
"web_search_tool": "web_search",
|
|
17236
17788
|
"search_context_size": "high",
|
|
17237
17789
|
"external_web_access": True,
|
|
17790
|
+
"spend_reserve_usd": 1.0,
|
|
17238
17791
|
"max_tool_calls": 8,
|
|
17239
17792
|
"max_output_tokens": 4200,
|
|
17240
17793
|
},
|
|
@@ -17267,6 +17820,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17267
17820
|
"secret_alias": "openai-primary",
|
|
17268
17821
|
"reasoning_effort": "high",
|
|
17269
17822
|
"text_verbosity": "medium",
|
|
17823
|
+
"spend_reserve_usd": 0.5,
|
|
17270
17824
|
"max_output_tokens": 5000,
|
|
17271
17825
|
},
|
|
17272
17826
|
{
|
|
@@ -17301,6 +17855,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17301
17855
|
"web_search": True,
|
|
17302
17856
|
"web_search_tool": "web_search_preview",
|
|
17303
17857
|
"background": False,
|
|
17858
|
+
"spend_reserve_usd": 1.5,
|
|
17304
17859
|
"max_tool_calls": 40,
|
|
17305
17860
|
"max_output_tokens": 12000,
|
|
17306
17861
|
},
|
|
@@ -17378,6 +17933,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17378
17933
|
"secret_alias": "openai-primary",
|
|
17379
17934
|
"reasoning_effort": "high",
|
|
17380
17935
|
"text_verbosity": "medium",
|
|
17936
|
+
"spend_reserve_usd": 0.5,
|
|
17381
17937
|
"max_output_tokens": 4200,
|
|
17382
17938
|
},
|
|
17383
17939
|
{
|
|
@@ -17396,6 +17952,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17396
17952
|
"web_search_tool": "web_search",
|
|
17397
17953
|
"search_context_size": "high",
|
|
17398
17954
|
"external_web_access": True,
|
|
17955
|
+
"spend_reserve_usd": 1.0,
|
|
17399
17956
|
"max_tool_calls": 8,
|
|
17400
17957
|
"max_output_tokens": 3600,
|
|
17401
17958
|
},
|
|
@@ -17413,6 +17970,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17413
17970
|
"web_search": True,
|
|
17414
17971
|
"web_search_tool": "web_search_preview",
|
|
17415
17972
|
"background": True,
|
|
17973
|
+
"spend_reserve_usd": 3.5,
|
|
17416
17974
|
"max_tool_calls": 40,
|
|
17417
17975
|
"max_output_tokens": 12000,
|
|
17418
17976
|
},
|
|
@@ -17700,6 +18258,312 @@ def _research_text_from_payload(payload: Any) -> str:
|
|
|
17700
18258
|
return ""
|
|
17701
18259
|
|
|
17702
18260
|
|
|
18261
|
+
def _as_positive_float(value: Any, *, field_name: str) -> float:
|
|
18262
|
+
try:
|
|
18263
|
+
parsed = float(value)
|
|
18264
|
+
except Exception as exc:
|
|
18265
|
+
raise RuntimeError(f"{field_name} must be a positive number.") from exc
|
|
18266
|
+
if parsed <= 0:
|
|
18267
|
+
raise RuntimeError(f"{field_name} must be greater than 0.")
|
|
18268
|
+
return parsed
|
|
18269
|
+
|
|
18270
|
+
|
|
18271
|
+
def _normalize_secret_spend_policy(raw_policy: Any) -> dict[str, Any]:
|
|
18272
|
+
if not isinstance(raw_policy, dict):
|
|
18273
|
+
return {}
|
|
18274
|
+
raw_daily_cap = raw_policy.get("daily_cap_usd", raw_policy.get("dailyCapUsd"))
|
|
18275
|
+
if raw_daily_cap in (None, ""):
|
|
18276
|
+
return {}
|
|
18277
|
+
try:
|
|
18278
|
+
daily_cap_usd = float(raw_daily_cap)
|
|
18279
|
+
except Exception:
|
|
18280
|
+
return {}
|
|
18281
|
+
if daily_cap_usd <= 0:
|
|
18282
|
+
return {}
|
|
18283
|
+
|
|
18284
|
+
raw_dashboard = raw_policy.get("dashboard_limit", raw_policy.get("dashboardLimit"))
|
|
18285
|
+
dashboard_limit = dict(raw_dashboard) if isinstance(raw_dashboard, dict) else {}
|
|
18286
|
+
status = str(
|
|
18287
|
+
dashboard_limit.get(
|
|
18288
|
+
"status",
|
|
18289
|
+
raw_policy.get("dashboard_status", raw_policy.get("dashboardStatus", "")),
|
|
18290
|
+
)
|
|
18291
|
+
or ""
|
|
18292
|
+
).strip()
|
|
18293
|
+
project_id = str(
|
|
18294
|
+
dashboard_limit.get(
|
|
18295
|
+
"project_id",
|
|
18296
|
+
dashboard_limit.get("projectId", raw_policy.get("dashboard_project_id", raw_policy.get("dashboardProjectId", ""))),
|
|
18297
|
+
)
|
|
18298
|
+
or ""
|
|
18299
|
+
).strip()
|
|
18300
|
+
dashboard_url = str(
|
|
18301
|
+
dashboard_limit.get(
|
|
18302
|
+
"dashboard_url",
|
|
18303
|
+
dashboard_limit.get("dashboardUrl", raw_policy.get("dashboard_url", raw_policy.get("dashboardUrl", ""))),
|
|
18304
|
+
)
|
|
18305
|
+
or ""
|
|
18306
|
+
).strip()
|
|
18307
|
+
normalized_dashboard: dict[str, Any] = {
|
|
18308
|
+
"provider": str(dashboard_limit.get("provider", raw_policy.get("provider", "openai")) or "openai").strip()
|
|
18309
|
+
or "openai",
|
|
18310
|
+
"status": status or "unconfirmed",
|
|
18311
|
+
}
|
|
18312
|
+
if project_id:
|
|
18313
|
+
normalized_dashboard["project_id"] = project_id
|
|
18314
|
+
if dashboard_url:
|
|
18315
|
+
normalized_dashboard["dashboard_url"] = dashboard_url
|
|
18316
|
+
|
|
18317
|
+
return {
|
|
18318
|
+
"schema_version": str(raw_policy.get("schema_version", raw_policy.get("schemaVersion", SECRET_SPEND_POLICY_SCHEMA_VERSION))).strip()
|
|
18319
|
+
or SECRET_SPEND_POLICY_SCHEMA_VERSION,
|
|
18320
|
+
"daily_cap_usd": round(daily_cap_usd, 6),
|
|
18321
|
+
"currency": str(raw_policy.get("currency", "USD") or "USD").strip().upper() or "USD",
|
|
18322
|
+
"scope": str(raw_policy.get("scope", "provider_project_key") or "provider_project_key").strip()
|
|
18323
|
+
or "provider_project_key",
|
|
18324
|
+
"enforcement": str(raw_policy.get("enforcement", "local_preflight_reservation") or "local_preflight_reservation").strip()
|
|
18325
|
+
or "local_preflight_reservation",
|
|
18326
|
+
"dashboard_limit": normalized_dashboard,
|
|
18327
|
+
"ledger_path": str(raw_policy.get("ledger_path", raw_policy.get("ledgerPath", str(_research_spend_ledger_path()))) or str(_research_spend_ledger_path())),
|
|
18328
|
+
}
|
|
18329
|
+
|
|
18330
|
+
|
|
18331
|
+
def _secret_spend_policy_payload(policy: dict[str, Any]) -> dict[str, Any]:
|
|
18332
|
+
normalized = _normalize_secret_spend_policy(policy)
|
|
18333
|
+
if not normalized:
|
|
18334
|
+
return {}
|
|
18335
|
+
dashboard_limit = normalized.get("dashboard_limit") if isinstance(normalized.get("dashboard_limit"), dict) else {}
|
|
18336
|
+
payload: dict[str, Any] = {
|
|
18337
|
+
"schemaVersion": str(normalized.get("schema_version", SECRET_SPEND_POLICY_SCHEMA_VERSION)).strip()
|
|
18338
|
+
or SECRET_SPEND_POLICY_SCHEMA_VERSION,
|
|
18339
|
+
"dailyCapUsd": normalized["daily_cap_usd"],
|
|
18340
|
+
"currency": str(normalized.get("currency", "USD")).strip() or "USD",
|
|
18341
|
+
"scope": str(normalized.get("scope", "provider_project_key")).strip() or "provider_project_key",
|
|
18342
|
+
"enforcement": str(normalized.get("enforcement", "local_preflight_reservation")).strip()
|
|
18343
|
+
or "local_preflight_reservation",
|
|
18344
|
+
"dashboardLimit": {
|
|
18345
|
+
"provider": str(dashboard_limit.get("provider", "openai")).strip() or "openai",
|
|
18346
|
+
"status": str(dashboard_limit.get("status", "unconfirmed")).strip() or "unconfirmed",
|
|
18347
|
+
},
|
|
18348
|
+
"ledgerPath": str(normalized.get("ledger_path", str(_research_spend_ledger_path()))).strip()
|
|
18349
|
+
or str(_research_spend_ledger_path()),
|
|
18350
|
+
}
|
|
18351
|
+
project_id = str(dashboard_limit.get("project_id", "")).strip()
|
|
18352
|
+
dashboard_url = str(dashboard_limit.get("dashboard_url", "")).strip()
|
|
18353
|
+
if project_id:
|
|
18354
|
+
payload["dashboardLimit"]["projectId"] = project_id
|
|
18355
|
+
if dashboard_url:
|
|
18356
|
+
payload["dashboardLimit"]["dashboardUrl"] = dashboard_url
|
|
18357
|
+
return payload
|
|
18358
|
+
|
|
18359
|
+
|
|
18360
|
+
def _secret_spend_policy_from_args(
|
|
18361
|
+
args: argparse.Namespace,
|
|
18362
|
+
existing_entry: dict[str, Any] | None = None,
|
|
18363
|
+
) -> dict[str, Any]:
|
|
18364
|
+
existing_policy = _normalize_secret_spend_policy(
|
|
18365
|
+
existing_entry.get("spend_policy", {}) if isinstance(existing_entry, dict) else {}
|
|
18366
|
+
)
|
|
18367
|
+
daily_cap_arg = getattr(args, "daily_spend_cap_usd", None)
|
|
18368
|
+
dashboard_status = str(getattr(args, "dashboard_spend_cap_status", "") or "").strip()
|
|
18369
|
+
dashboard_project_id = str(getattr(args, "dashboard_project_id", "") or "").strip()
|
|
18370
|
+
dashboard_url = str(getattr(args, "dashboard_url", "") or "").strip()
|
|
18371
|
+
if daily_cap_arg in (None, "") and not dashboard_status and not dashboard_project_id and not dashboard_url:
|
|
18372
|
+
return existing_policy
|
|
18373
|
+
|
|
18374
|
+
if daily_cap_arg in (None, ""):
|
|
18375
|
+
if not existing_policy:
|
|
18376
|
+
raise RuntimeError("--daily-spend-cap-usd is required when creating a spend policy.")
|
|
18377
|
+
daily_cap_usd = float(existing_policy["daily_cap_usd"])
|
|
18378
|
+
else:
|
|
18379
|
+
daily_cap_usd = _as_positive_float(daily_cap_arg, field_name="--daily-spend-cap-usd")
|
|
18380
|
+
|
|
18381
|
+
existing_dashboard = (
|
|
18382
|
+
existing_policy.get("dashboard_limit")
|
|
18383
|
+
if isinstance(existing_policy.get("dashboard_limit"), dict)
|
|
18384
|
+
else {}
|
|
18385
|
+
)
|
|
18386
|
+
dashboard_limit = dict(existing_dashboard)
|
|
18387
|
+
if dashboard_status:
|
|
18388
|
+
dashboard_limit["status"] = dashboard_status
|
|
18389
|
+
elif "status" not in dashboard_limit:
|
|
18390
|
+
dashboard_limit["status"] = "unconfirmed"
|
|
18391
|
+
if dashboard_project_id:
|
|
18392
|
+
dashboard_limit["project_id"] = dashboard_project_id
|
|
18393
|
+
if dashboard_url:
|
|
18394
|
+
dashboard_limit["dashboard_url"] = dashboard_url
|
|
18395
|
+
dashboard_limit["provider"] = str(dashboard_limit.get("provider", getattr(args, "provider", "openai")) or "openai").strip() or "openai"
|
|
18396
|
+
|
|
18397
|
+
return _normalize_secret_spend_policy(
|
|
18398
|
+
{
|
|
18399
|
+
**existing_policy,
|
|
18400
|
+
"schema_version": SECRET_SPEND_POLICY_SCHEMA_VERSION,
|
|
18401
|
+
"daily_cap_usd": daily_cap_usd,
|
|
18402
|
+
"currency": "USD",
|
|
18403
|
+
"scope": "provider_project_key",
|
|
18404
|
+
"enforcement": "local_preflight_reservation",
|
|
18405
|
+
"dashboard_limit": dashboard_limit,
|
|
18406
|
+
"ledger_path": str(_research_spend_ledger_path()),
|
|
18407
|
+
}
|
|
18408
|
+
)
|
|
18409
|
+
|
|
18410
|
+
|
|
18411
|
+
def _research_lane_spend_reserve_usd(lane: dict[str, Any]) -> float:
|
|
18412
|
+
if "spend_reserve_usd" in lane:
|
|
18413
|
+
try:
|
|
18414
|
+
reserve = float(lane.get("spend_reserve_usd", 0) or 0)
|
|
18415
|
+
except Exception:
|
|
18416
|
+
reserve = 0.0
|
|
18417
|
+
return max(0.0, round(reserve, 6))
|
|
18418
|
+
model = str(lane.get("model", "") or "").strip().lower()
|
|
18419
|
+
if "deep-research" in model:
|
|
18420
|
+
return 3.5
|
|
18421
|
+
if bool(lane.get("web_search", False)) or lane.get("tools"):
|
|
18422
|
+
return 1.0
|
|
18423
|
+
effort = str(lane.get("reasoning_effort", "") or "").strip().lower()
|
|
18424
|
+
if effort in {"high", "xhigh"}:
|
|
18425
|
+
return 0.5
|
|
18426
|
+
return 0.25
|
|
18427
|
+
|
|
18428
|
+
|
|
18429
|
+
def _research_spend_policy_entry_for_lane(lane: dict[str, Any]) -> tuple[dict[str, Any] | None, str]:
|
|
18430
|
+
secret_alias = str(lane.get("secret_alias", "") or "").strip()
|
|
18431
|
+
provider = str(lane.get("provider", "") or "").strip()
|
|
18432
|
+
if not secret_alias and not provider:
|
|
18433
|
+
return None, "no secret alias or provider configured"
|
|
18434
|
+
try:
|
|
18435
|
+
return (
|
|
18436
|
+
_select_keychain_entry(
|
|
18437
|
+
secret_ref=secret_alias,
|
|
18438
|
+
provider=provider,
|
|
18439
|
+
world_id="",
|
|
18440
|
+
idea_id="",
|
|
18441
|
+
),
|
|
18442
|
+
"",
|
|
18443
|
+
)
|
|
18444
|
+
except Exception as exc:
|
|
18445
|
+
return None, str(exc)
|
|
18446
|
+
|
|
18447
|
+
|
|
18448
|
+
def _research_spend_ledger_today_total(
|
|
18449
|
+
*,
|
|
18450
|
+
date_utc: str,
|
|
18451
|
+
provider: str,
|
|
18452
|
+
secret_alias: str,
|
|
18453
|
+
) -> float:
|
|
18454
|
+
ledger = _load_research_spend_ledger()
|
|
18455
|
+
total = 0.0
|
|
18456
|
+
for row in ledger.get("records", []):
|
|
18457
|
+
if not isinstance(row, dict):
|
|
18458
|
+
continue
|
|
18459
|
+
if str(row.get("date_utc", "")).strip() != date_utc:
|
|
18460
|
+
continue
|
|
18461
|
+
if provider and str(row.get("provider", "")).strip() != provider:
|
|
18462
|
+
continue
|
|
18463
|
+
if secret_alias and str(row.get("secret_alias", "")).strip() != secret_alias:
|
|
18464
|
+
continue
|
|
18465
|
+
event = str(row.get("event", "")).strip()
|
|
18466
|
+
if event != "reserved":
|
|
18467
|
+
continue
|
|
18468
|
+
try:
|
|
18469
|
+
total += float(row.get("amount_usd", row.get("reserve_usd", 0)) or 0)
|
|
18470
|
+
except Exception:
|
|
18471
|
+
continue
|
|
18472
|
+
return round(total, 6)
|
|
18473
|
+
|
|
18474
|
+
|
|
18475
|
+
def _research_openai_spend_preflight(
|
|
18476
|
+
lane: dict[str, Any],
|
|
18477
|
+
*,
|
|
18478
|
+
secret_source: str,
|
|
18479
|
+
) -> dict[str, Any]:
|
|
18480
|
+
provider = str(lane.get("provider", "") or "").strip()
|
|
18481
|
+
secret_alias = str(lane.get("secret_alias", "") or "").strip()
|
|
18482
|
+
reserve_usd = _research_lane_spend_reserve_usd(lane)
|
|
18483
|
+
entry, entry_issue = _research_spend_policy_entry_for_lane(lane)
|
|
18484
|
+
policy = _normalize_secret_spend_policy(entry.get("spend_policy", {}) if isinstance(entry, dict) else {})
|
|
18485
|
+
date_utc = dt.datetime.now(dt.timezone.utc).date().isoformat()
|
|
18486
|
+
base = {
|
|
18487
|
+
"schema_version": RESEARCH_SPEND_LEDGER_SCHEMA_VERSION,
|
|
18488
|
+
"provider": provider,
|
|
18489
|
+
"secret_alias": secret_alias,
|
|
18490
|
+
"secret_source": secret_source,
|
|
18491
|
+
"date_utc": date_utc,
|
|
18492
|
+
"reserve_usd": reserve_usd,
|
|
18493
|
+
"ledger_path": str(_research_spend_ledger_path()),
|
|
18494
|
+
}
|
|
18495
|
+
if not policy:
|
|
18496
|
+
return {
|
|
18497
|
+
**base,
|
|
18498
|
+
"allowed": True,
|
|
18499
|
+
"policy_source": "",
|
|
18500
|
+
"reason": entry_issue or "no spend policy configured for this local keychain entry",
|
|
18501
|
+
}
|
|
18502
|
+
|
|
18503
|
+
reserved_today = _research_spend_ledger_today_total(
|
|
18504
|
+
date_utc=date_utc,
|
|
18505
|
+
provider=provider,
|
|
18506
|
+
secret_alias=secret_alias,
|
|
18507
|
+
)
|
|
18508
|
+
daily_cap_usd = float(policy["daily_cap_usd"])
|
|
18509
|
+
remaining_before = round(max(0.0, daily_cap_usd - reserved_today), 6)
|
|
18510
|
+
remaining_after = round(daily_cap_usd - reserved_today - reserve_usd, 6)
|
|
18511
|
+
allowed = remaining_after >= -0.000001
|
|
18512
|
+
dashboard_limit = policy.get("dashboard_limit") if isinstance(policy.get("dashboard_limit"), dict) else {}
|
|
18513
|
+
return {
|
|
18514
|
+
**base,
|
|
18515
|
+
"allowed": allowed,
|
|
18516
|
+
"policy_source": "keychain",
|
|
18517
|
+
"daily_cap_usd": round(daily_cap_usd, 6),
|
|
18518
|
+
"currency": str(policy.get("currency", "USD")).strip() or "USD",
|
|
18519
|
+
"reserved_today_usd": reserved_today,
|
|
18520
|
+
"remaining_before_reserve_usd": remaining_before,
|
|
18521
|
+
"remaining_after_reserve_usd": remaining_after,
|
|
18522
|
+
"dashboard_limit": dict(dashboard_limit),
|
|
18523
|
+
"reason": "within daily spend cap" if allowed else "daily spend cap would be exceeded",
|
|
18524
|
+
}
|
|
18525
|
+
|
|
18526
|
+
|
|
18527
|
+
def _research_append_spend_ledger_record(
|
|
18528
|
+
lane: dict[str, Any],
|
|
18529
|
+
spend_preflight: dict[str, Any],
|
|
18530
|
+
*,
|
|
18531
|
+
event: str,
|
|
18532
|
+
status: str = "",
|
|
18533
|
+
provider_response_id: str = "",
|
|
18534
|
+
usage: dict[str, Any] | None = None,
|
|
18535
|
+
) -> dict[str, Any]:
|
|
18536
|
+
if spend_preflight.get("policy_source") != "keychain":
|
|
18537
|
+
return {}
|
|
18538
|
+
amount_usd = round(float(spend_preflight.get("reserve_usd", 0) or 0), 6) if event == "reserved" else 0.0
|
|
18539
|
+
record = {
|
|
18540
|
+
"id": f"spend-{uuid.uuid4().hex[:12]}",
|
|
18541
|
+
"schema_version": RESEARCH_SPEND_LEDGER_SCHEMA_VERSION,
|
|
18542
|
+
"recorded_at_utc": _now_utc(),
|
|
18543
|
+
"date_utc": str(spend_preflight.get("date_utc", "")).strip(),
|
|
18544
|
+
"event": event,
|
|
18545
|
+
"provider": str(spend_preflight.get("provider", lane.get("provider", ""))).strip(),
|
|
18546
|
+
"secret_alias": str(spend_preflight.get("secret_alias", lane.get("secret_alias", ""))).strip(),
|
|
18547
|
+
"lane_id": str(lane.get("lane_id", "")).strip(),
|
|
18548
|
+
"call_moment": str(lane.get("call_moment", lane.get("lane_id", ""))).strip(),
|
|
18549
|
+
"model": str(lane.get("model", "")).strip(),
|
|
18550
|
+
"amount_usd": amount_usd,
|
|
18551
|
+
"reserve_usd": round(float(spend_preflight.get("reserve_usd", 0) or 0), 6),
|
|
18552
|
+
"currency": str(spend_preflight.get("currency", "USD")).strip() or "USD",
|
|
18553
|
+
"daily_cap_usd": spend_preflight.get("daily_cap_usd"),
|
|
18554
|
+
"status": status,
|
|
18555
|
+
"provider_response_id": provider_response_id,
|
|
18556
|
+
}
|
|
18557
|
+
if isinstance(usage, dict) and usage:
|
|
18558
|
+
record["usage"] = usage
|
|
18559
|
+
ledger = _load_research_spend_ledger()
|
|
18560
|
+
records = ledger.get("records") if isinstance(ledger.get("records"), list) else []
|
|
18561
|
+
records.append(record)
|
|
18562
|
+
ledger["records"] = records
|
|
18563
|
+
_save_research_spend_ledger(ledger)
|
|
18564
|
+
return record
|
|
18565
|
+
|
|
18566
|
+
|
|
17703
18567
|
def _research_lane_api_call_plan(
|
|
17704
18568
|
lane: dict[str, Any],
|
|
17705
18569
|
*,
|
|
@@ -17709,12 +18573,13 @@ def _research_lane_api_call_plan(
|
|
|
17709
18573
|
reason: str = "",
|
|
17710
18574
|
request_body_keys: Sequence[str] | None = None,
|
|
17711
18575
|
tools: Sequence[str] | None = None,
|
|
18576
|
+
spend_preflight: dict[str, Any] | None = None,
|
|
17712
18577
|
) -> dict[str, Any]:
|
|
17713
18578
|
adapter = str(lane.get("adapter", "")).strip()
|
|
17714
18579
|
provider = str(lane.get("provider", "")).strip()
|
|
17715
18580
|
env_var = str(lane.get("env_var", "")).strip()
|
|
17716
18581
|
secret_alias = str(lane.get("secret_alias", "")).strip()
|
|
17717
|
-
|
|
18582
|
+
plan = {
|
|
17718
18583
|
"call_moment": str(lane.get("call_moment", lane.get("lane_id", ""))).strip(),
|
|
17719
18584
|
"calls_api": adapter in {"openai_responses", "anthropic_messages", "xai_chat_completions", "chimera_cli"},
|
|
17720
18585
|
"called": bool(called),
|
|
@@ -17732,6 +18597,9 @@ def _research_lane_api_call_plan(
|
|
|
17732
18597
|
"tools": [str(row) for row in tools] if tools else [],
|
|
17733
18598
|
"reason": reason,
|
|
17734
18599
|
}
|
|
18600
|
+
if spend_preflight is not None:
|
|
18601
|
+
plan["spend_preflight"] = spend_preflight
|
|
18602
|
+
return plan
|
|
17735
18603
|
|
|
17736
18604
|
|
|
17737
18605
|
def _research_fixture_lane_result(
|
|
@@ -18101,6 +18969,49 @@ def _research_run_openai_lane(
|
|
|
18101
18969
|
for row in body.get("tools", [])
|
|
18102
18970
|
if isinstance(row, dict) and str(row.get("type", "")).strip()
|
|
18103
18971
|
]
|
|
18972
|
+
spend_preflight = _research_openai_spend_preflight(lane, secret_source=secret_source)
|
|
18973
|
+
if not bool(spend_preflight.get("allowed", True)):
|
|
18974
|
+
finished_at_utc = _now_utc()
|
|
18975
|
+
return {
|
|
18976
|
+
"schema_version": RESEARCH_RUN_SCHEMA_VERSION,
|
|
18977
|
+
"lane_id": lane["lane_id"],
|
|
18978
|
+
"label": lane.get("label", lane["lane_id"]),
|
|
18979
|
+
"provider": lane.get("provider", ""),
|
|
18980
|
+
"model": lane.get("model", ""),
|
|
18981
|
+
"adapter": "openai_responses",
|
|
18982
|
+
"call_moment": lane.get("call_moment", lane["lane_id"]),
|
|
18983
|
+
"api_call": _research_lane_api_call_plan(
|
|
18984
|
+
lane,
|
|
18985
|
+
execute=True,
|
|
18986
|
+
called=False,
|
|
18987
|
+
secret_source=secret_source,
|
|
18988
|
+
reason=str(spend_preflight.get("reason", "spend preflight blocked provider call")),
|
|
18989
|
+
request_body_keys=body.keys(),
|
|
18990
|
+
tools=tool_types,
|
|
18991
|
+
spend_preflight=spend_preflight,
|
|
18992
|
+
),
|
|
18993
|
+
"status": "skipped",
|
|
18994
|
+
"started_at_utc": started_at_utc,
|
|
18995
|
+
"finished_at_utc": finished_at_utc,
|
|
18996
|
+
"duration_ms": _duration_ms(started_at_utc, finished_at_utc),
|
|
18997
|
+
"text": "",
|
|
18998
|
+
"spend_preflight": spend_preflight,
|
|
18999
|
+
"notes": [
|
|
19000
|
+
str(spend_preflight.get("reason", "Spend preflight blocked provider call.")),
|
|
19001
|
+
f"Secret supplied from {secret_source}; secret value was not persisted.",
|
|
19002
|
+
],
|
|
19003
|
+
}
|
|
19004
|
+
reservation = _research_append_spend_ledger_record(
|
|
19005
|
+
lane,
|
|
19006
|
+
spend_preflight,
|
|
19007
|
+
event="reserved",
|
|
19008
|
+
status="pending",
|
|
19009
|
+
)
|
|
19010
|
+
if reservation:
|
|
19011
|
+
spend_preflight = {
|
|
19012
|
+
**spend_preflight,
|
|
19013
|
+
"reservation_id": str(reservation.get("id", "")).strip(),
|
|
19014
|
+
}
|
|
18104
19015
|
api_call = _research_lane_api_call_plan(
|
|
18105
19016
|
lane,
|
|
18106
19017
|
execute=True,
|
|
@@ -18108,6 +19019,7 @@ def _research_run_openai_lane(
|
|
|
18108
19019
|
secret_source=secret_source,
|
|
18109
19020
|
request_body_keys=body.keys(),
|
|
18110
19021
|
tools=tool_types,
|
|
19022
|
+
spend_preflight=spend_preflight,
|
|
18111
19023
|
)
|
|
18112
19024
|
|
|
18113
19025
|
request = urlrequest.Request(
|
|
@@ -18172,6 +19084,14 @@ def _research_run_openai_lane(
|
|
|
18172
19084
|
status = "complete" if response_status == "completed" and text else response_status or "complete"
|
|
18173
19085
|
if status == "in_progress":
|
|
18174
19086
|
text = text or "OpenAI deep research started in background mode; poll the response id outside ORP for completion."
|
|
19087
|
+
_research_append_spend_ledger_record(
|
|
19088
|
+
lane,
|
|
19089
|
+
spend_preflight,
|
|
19090
|
+
event="usage",
|
|
19091
|
+
status=status,
|
|
19092
|
+
provider_response_id=str(response_payload.get("id", "")).strip(),
|
|
19093
|
+
usage=response_payload.get("usage") if isinstance(response_payload.get("usage"), dict) else None,
|
|
19094
|
+
)
|
|
18175
19095
|
return {
|
|
18176
19096
|
"schema_version": RESEARCH_RUN_SCHEMA_VERSION,
|
|
18177
19097
|
"lane_id": lane["lane_id"],
|
|
@@ -18181,6 +19101,7 @@ def _research_run_openai_lane(
|
|
|
18181
19101
|
"adapter": "openai_responses",
|
|
18182
19102
|
"call_moment": lane.get("call_moment", lane["lane_id"]),
|
|
18183
19103
|
"api_call": api_call,
|
|
19104
|
+
"spend_preflight": spend_preflight,
|
|
18184
19105
|
"status": status,
|
|
18185
19106
|
"started_at_utc": started_at_utc,
|
|
18186
19107
|
"finished_at_utc": finished_at_utc,
|
|
@@ -22632,6 +23553,12 @@ def _print_secret_human(
|
|
|
22632
23553
|
source: str = "",
|
|
22633
23554
|
) -> None:
|
|
22634
23555
|
bindings = _secret_bindings(secret)
|
|
23556
|
+
spend_policy = _normalize_secret_spend_policy(secret.get("spendPolicy", secret.get("spend_policy", {})))
|
|
23557
|
+
dashboard_limit = (
|
|
23558
|
+
spend_policy.get("dashboard_limit")
|
|
23559
|
+
if isinstance(spend_policy.get("dashboard_limit"), dict)
|
|
23560
|
+
else {}
|
|
23561
|
+
)
|
|
22635
23562
|
_print_pairs(
|
|
22636
23563
|
[
|
|
22637
23564
|
("secret.id", str(secret.get("id", "")).strip()),
|
|
@@ -22644,6 +23571,9 @@ def _print_secret_human(
|
|
|
22644
23571
|
("secret.preview", str(secret.get("valuePreview", "")).strip()),
|
|
22645
23572
|
("secret.version", str(secret.get("valueVersion", "")).strip()),
|
|
22646
23573
|
("secret.status", str(secret.get("status", "")).strip()),
|
|
23574
|
+
("secret.spend_policy.daily_cap_usd", str(spend_policy.get("daily_cap_usd", ""))),
|
|
23575
|
+
("secret.spend_policy.enforcement", str(spend_policy.get("enforcement", ""))),
|
|
23576
|
+
("secret.spend_policy.dashboard_status", str(dashboard_limit.get("status", ""))),
|
|
22647
23577
|
("secret.binding_count", len(bindings)),
|
|
22648
23578
|
("secret.last_used_at", str(secret.get("lastUsedAt", "")).strip()),
|
|
22649
23579
|
("secret.rotated_at", str(secret.get("rotatedAt", "")).strip()),
|
|
@@ -22754,7 +23684,7 @@ def _build_keychain_registry_entry(
|
|
|
22754
23684
|
bindings = [_normalize_secret_binding_summary(row) for row in _secret_bindings(secret)]
|
|
22755
23685
|
if binding:
|
|
22756
23686
|
bindings = _merge_secret_binding_summaries(bindings, [binding])
|
|
22757
|
-
|
|
23687
|
+
entry = {
|
|
22758
23688
|
"secret_id": str(secret.get("id", "")).strip(),
|
|
22759
23689
|
"alias": str(secret.get("alias", "")).strip(),
|
|
22760
23690
|
"label": str(secret.get("label", "")).strip(),
|
|
@@ -22771,11 +23701,15 @@ def _build_keychain_registry_entry(
|
|
|
22771
23701
|
"bindings": bindings,
|
|
22772
23702
|
"last_synced_at_utc": _now_utc(),
|
|
22773
23703
|
}
|
|
23704
|
+
spend_policy = _normalize_secret_spend_policy(secret.get("spendPolicy", secret.get("spend_policy", {})))
|
|
23705
|
+
if spend_policy:
|
|
23706
|
+
entry["spend_policy"] = spend_policy
|
|
23707
|
+
return entry
|
|
22774
23708
|
|
|
22775
23709
|
|
|
22776
23710
|
def _secret_payload_from_keychain_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
22777
23711
|
bindings = entry.get("bindings") if isinstance(entry.get("bindings"), list) else []
|
|
22778
|
-
|
|
23712
|
+
payload = {
|
|
22779
23713
|
"id": str(entry.get("secret_id", "")).strip(),
|
|
22780
23714
|
"alias": str(entry.get("alias", "")).strip(),
|
|
22781
23715
|
"label": str(entry.get("label", "")).strip(),
|
|
@@ -22792,6 +23726,12 @@ def _secret_payload_from_keychain_entry(entry: dict[str, Any]) -> dict[str, Any]
|
|
|
22792
23726
|
if isinstance(row, dict)
|
|
22793
23727
|
],
|
|
22794
23728
|
}
|
|
23729
|
+
spend_policy = _secret_spend_policy_payload(
|
|
23730
|
+
entry.get("spend_policy", {}) if isinstance(entry.get("spend_policy"), dict) else {}
|
|
23731
|
+
)
|
|
23732
|
+
if spend_policy:
|
|
23733
|
+
payload["spendPolicy"] = spend_policy
|
|
23734
|
+
return payload
|
|
22795
23735
|
|
|
22796
23736
|
|
|
22797
23737
|
def _upsert_keychain_secret_registry_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -23005,8 +23945,9 @@ def _build_local_keychain_secret_from_args(args: argparse.Namespace, existing_en
|
|
|
23005
23945
|
kind = str(getattr(args, "kind", "api_key") or "api_key").strip() or "api_key"
|
|
23006
23946
|
username = getattr(args, "username", None)
|
|
23007
23947
|
env_var_name = getattr(args, "env_var_name", None)
|
|
23948
|
+
spend_policy = _secret_spend_policy_from_args(args, existing_entry)
|
|
23008
23949
|
now = _now_utc()
|
|
23009
|
-
|
|
23950
|
+
secret = {
|
|
23010
23951
|
"id": str(existing_entry.get("secret_id", "") if existing_entry else "").strip() or f"local-{uuid.uuid4().hex[:12]}",
|
|
23011
23952
|
"alias": alias,
|
|
23012
23953
|
"label": label,
|
|
@@ -23026,6 +23967,9 @@ def _build_local_keychain_secret_from_args(args: argparse.Namespace, existing_en
|
|
|
23026
23967
|
"rotatedAt": now,
|
|
23027
23968
|
"updatedAt": now,
|
|
23028
23969
|
}
|
|
23970
|
+
if spend_policy:
|
|
23971
|
+
secret["spendPolicy"] = spend_policy
|
|
23972
|
+
return secret
|
|
23029
23973
|
|
|
23030
23974
|
|
|
23031
23975
|
def _try_get_secret_by_ref(args: argparse.Namespace, secret_ref: str) -> dict[str, Any] | None:
|
|
@@ -24868,6 +25812,47 @@ def cmd_secrets_keychain_add(args: argparse.Namespace) -> int:
|
|
|
24868
25812
|
return 0
|
|
24869
25813
|
|
|
24870
25814
|
|
|
25815
|
+
def cmd_secrets_keychain_spend_policy(args: argparse.Namespace) -> int:
|
|
25816
|
+
secret_ref = str(getattr(args, "secret_ref", "") or "").strip()
|
|
25817
|
+
if not secret_ref:
|
|
25818
|
+
raise RuntimeError("Secret reference is required.")
|
|
25819
|
+
items = _list_keychain_registry_entries(secret_ref=secret_ref)
|
|
25820
|
+
if not items:
|
|
25821
|
+
raise RuntimeError("No matching local Keychain secret was found.")
|
|
25822
|
+
entry = dict(items[0])
|
|
25823
|
+
spend_policy = _secret_spend_policy_from_args(args, entry)
|
|
25824
|
+
if not spend_policy:
|
|
25825
|
+
raise RuntimeError("--daily-spend-cap-usd is required when setting a spend policy.")
|
|
25826
|
+
entry["spend_policy"] = spend_policy
|
|
25827
|
+
entry["last_synced_at_utc"] = _now_utc()
|
|
25828
|
+
entry = _upsert_keychain_secret_registry_entry(entry)
|
|
25829
|
+
result = {
|
|
25830
|
+
"ok": True,
|
|
25831
|
+
"secret": _secret_payload_from_keychain_entry(entry),
|
|
25832
|
+
"entry": entry,
|
|
25833
|
+
"registry_path": str(_keychain_secret_registry_path()),
|
|
25834
|
+
"keychain_service": str(entry.get("keychain_service", "")).strip(),
|
|
25835
|
+
"keychain_account": str(entry.get("keychain_account", "")).strip(),
|
|
25836
|
+
"source": "keychain",
|
|
25837
|
+
}
|
|
25838
|
+
if args.json_output:
|
|
25839
|
+
_print_json(result)
|
|
25840
|
+
else:
|
|
25841
|
+
_print_secret_human(
|
|
25842
|
+
result["secret"],
|
|
25843
|
+
include_bindings=True,
|
|
25844
|
+
source="keychain",
|
|
25845
|
+
)
|
|
25846
|
+
_print_pairs(
|
|
25847
|
+
[
|
|
25848
|
+
("keychain.service", result["keychain_service"]),
|
|
25849
|
+
("keychain.account", result["keychain_account"]),
|
|
25850
|
+
("registry.path", result["registry_path"]),
|
|
25851
|
+
]
|
|
25852
|
+
)
|
|
25853
|
+
return 0
|
|
25854
|
+
|
|
25855
|
+
|
|
24871
25856
|
def cmd_secrets_keychain_list(args: argparse.Namespace) -> int:
|
|
24872
25857
|
provider = str(getattr(args, "provider", "") or "").strip()
|
|
24873
25858
|
world_id, idea_id = _resolve_secret_scope_from_args(
|
|
@@ -24902,6 +25887,14 @@ def cmd_secrets_keychain_list(args: argparse.Namespace) -> int:
|
|
|
24902
25887
|
)
|
|
24903
25888
|
for row in items:
|
|
24904
25889
|
print("---")
|
|
25890
|
+
spend_policy = _normalize_secret_spend_policy(
|
|
25891
|
+
row.get("spend_policy", {}) if isinstance(row.get("spend_policy"), dict) else {}
|
|
25892
|
+
)
|
|
25893
|
+
dashboard_limit = (
|
|
25894
|
+
spend_policy.get("dashboard_limit")
|
|
25895
|
+
if isinstance(spend_policy.get("dashboard_limit"), dict)
|
|
25896
|
+
else {}
|
|
25897
|
+
)
|
|
24905
25898
|
_print_pairs(
|
|
24906
25899
|
[
|
|
24907
25900
|
("secret.id", str(row.get("secret_id", "")).strip()),
|
|
@@ -24911,6 +25904,8 @@ def cmd_secrets_keychain_list(args: argparse.Namespace) -> int:
|
|
|
24911
25904
|
("secret.kind", str(row.get("kind", "")).strip()),
|
|
24912
25905
|
("secret.env_var_name", str(row.get("env_var_name", "")).strip()),
|
|
24913
25906
|
("secret.status", str(row.get("status", "")).strip()),
|
|
25907
|
+
("secret.spend_policy.daily_cap_usd", str(spend_policy.get("daily_cap_usd", ""))),
|
|
25908
|
+
("secret.spend_policy.dashboard_status", str(dashboard_limit.get("status", ""))),
|
|
24914
25909
|
("secret.binding_count", len(row.get("bindings", [])) if isinstance(row.get("bindings"), list) else 0),
|
|
24915
25910
|
("keychain.service", str(row.get("keychain_service", "")).strip()),
|
|
24916
25911
|
("keychain.account", str(row.get("keychain_account", "")).strip()),
|
|
@@ -27532,6 +28527,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
27532
28527
|
action="store_true",
|
|
27533
28528
|
help="Read the secret value from --env-var-name in the current process environment",
|
|
27534
28529
|
)
|
|
28530
|
+
s_secrets_keychain_add.add_argument(
|
|
28531
|
+
"--daily-spend-cap-usd",
|
|
28532
|
+
type=float,
|
|
28533
|
+
default=None,
|
|
28534
|
+
help="Optional local daily USD spend cap for provider calls that use this key",
|
|
28535
|
+
)
|
|
28536
|
+
s_secrets_keychain_add.add_argument(
|
|
28537
|
+
"--dashboard-spend-cap-status",
|
|
28538
|
+
choices=["unconfirmed", "confirmed", "not_applicable"],
|
|
28539
|
+
default="",
|
|
28540
|
+
help="Record whether the matching provider dashboard spend limit has been confirmed",
|
|
28541
|
+
)
|
|
28542
|
+
s_secrets_keychain_add.add_argument(
|
|
28543
|
+
"--dashboard-project-id",
|
|
28544
|
+
default="",
|
|
28545
|
+
help="Optional provider dashboard project id associated with this key/cap",
|
|
28546
|
+
)
|
|
28547
|
+
s_secrets_keychain_add.add_argument(
|
|
28548
|
+
"--dashboard-url",
|
|
28549
|
+
default="",
|
|
28550
|
+
help="Optional provider dashboard URL where the spend cap is managed",
|
|
28551
|
+
)
|
|
27535
28552
|
add_secret_scope_flags(s_secrets_keychain_add)
|
|
27536
28553
|
s_secrets_keychain_add.add_argument("--purpose", default="", help="Optional project usage note when binding")
|
|
27537
28554
|
s_secrets_keychain_add.add_argument(
|
|
@@ -27542,6 +28559,36 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
27542
28559
|
add_json_flag(s_secrets_keychain_add)
|
|
27543
28560
|
s_secrets_keychain_add.set_defaults(func=cmd_secrets_keychain_add, json_output=False)
|
|
27544
28561
|
|
|
28562
|
+
s_secrets_keychain_spend_policy = secrets_sub.add_parser(
|
|
28563
|
+
"keychain-spend-policy",
|
|
28564
|
+
help="Attach or update local spend policy metadata for an existing Keychain secret",
|
|
28565
|
+
)
|
|
28566
|
+
s_secrets_keychain_spend_policy.add_argument("secret_ref", help="Secret alias or id")
|
|
28567
|
+
s_secrets_keychain_spend_policy.add_argument(
|
|
28568
|
+
"--daily-spend-cap-usd",
|
|
28569
|
+
type=float,
|
|
28570
|
+
required=True,
|
|
28571
|
+
help="Local daily USD spend cap for provider calls that use this key",
|
|
28572
|
+
)
|
|
28573
|
+
s_secrets_keychain_spend_policy.add_argument(
|
|
28574
|
+
"--dashboard-spend-cap-status",
|
|
28575
|
+
choices=["unconfirmed", "confirmed", "not_applicable"],
|
|
28576
|
+
default="",
|
|
28577
|
+
help="Record whether the matching provider dashboard spend limit has been confirmed",
|
|
28578
|
+
)
|
|
28579
|
+
s_secrets_keychain_spend_policy.add_argument(
|
|
28580
|
+
"--dashboard-project-id",
|
|
28581
|
+
default="",
|
|
28582
|
+
help="Optional provider dashboard project id associated with this key/cap",
|
|
28583
|
+
)
|
|
28584
|
+
s_secrets_keychain_spend_policy.add_argument(
|
|
28585
|
+
"--dashboard-url",
|
|
28586
|
+
default="",
|
|
28587
|
+
help="Optional provider dashboard URL where the spend cap is managed",
|
|
28588
|
+
)
|
|
28589
|
+
add_json_flag(s_secrets_keychain_spend_policy)
|
|
28590
|
+
s_secrets_keychain_spend_policy.set_defaults(func=cmd_secrets_keychain_spend_policy, json_output=False)
|
|
28591
|
+
|
|
27545
28592
|
s_secrets_keychain_list = secrets_sub.add_parser(
|
|
27546
28593
|
"keychain-list",
|
|
27547
28594
|
help="List local macOS Keychain copies known to ORP on this machine",
|
|
@@ -28471,6 +29518,78 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
28471
29518
|
default="",
|
|
28472
29519
|
help="Optional shared umbrella projects root used to link this repo's AGENTS.md and CLAUDE.md back to a parent guide",
|
|
28473
29520
|
)
|
|
29521
|
+
s_init.add_argument(
|
|
29522
|
+
"--project-startup",
|
|
29523
|
+
action="store_true",
|
|
29524
|
+
help="Run the common new-project bootstrap: private GitHub remote when --github-repo is set, workspace main tracking, and Clawdad registration when installed",
|
|
29525
|
+
)
|
|
29526
|
+
s_init.add_argument(
|
|
29527
|
+
"--private-github",
|
|
29528
|
+
"--github-private",
|
|
29529
|
+
"--create-private-github-remote",
|
|
29530
|
+
dest="private_github",
|
|
29531
|
+
action="store_true",
|
|
29532
|
+
help="Create a private GitHub repository/remote with gh repo create when origin is absent",
|
|
29533
|
+
)
|
|
29534
|
+
s_init.add_argument(
|
|
29535
|
+
"--track-workspace-main",
|
|
29536
|
+
"--workspace-main",
|
|
29537
|
+
dest="track_workspace_main",
|
|
29538
|
+
action="store_true",
|
|
29539
|
+
help="Track this path in the ORP workspace ledger (default workspace: main)",
|
|
29540
|
+
)
|
|
29541
|
+
s_init.add_argument(
|
|
29542
|
+
"--workspace-name",
|
|
29543
|
+
default="main",
|
|
29544
|
+
help="Workspace ledger name for startup tracking (default: main)",
|
|
29545
|
+
)
|
|
29546
|
+
s_init.add_argument(
|
|
29547
|
+
"--workspace-title",
|
|
29548
|
+
default="",
|
|
29549
|
+
help="Optional title for the workspace tab recorded during startup",
|
|
29550
|
+
)
|
|
29551
|
+
s_init.add_argument(
|
|
29552
|
+
"--workspace-bootstrap-command",
|
|
29553
|
+
default="",
|
|
29554
|
+
help="Optional bootstrap command saved in the workspace tab, for example npm install",
|
|
29555
|
+
)
|
|
29556
|
+
s_init.add_argument(
|
|
29557
|
+
"--workspace-append",
|
|
29558
|
+
action="store_true",
|
|
29559
|
+
help="Append a new workspace session entry instead of replacing/upserting the path entry",
|
|
29560
|
+
)
|
|
29561
|
+
s_init.add_argument(
|
|
29562
|
+
"--with-clawdad",
|
|
29563
|
+
"--clawdad-delegation",
|
|
29564
|
+
dest="with_clawdad",
|
|
29565
|
+
action="store_true",
|
|
29566
|
+
help="Scaffold a Clawdad delegate brief and register the project when clawdad is installed",
|
|
29567
|
+
)
|
|
29568
|
+
s_init.add_argument(
|
|
29569
|
+
"--clawdad-slug",
|
|
29570
|
+
default="",
|
|
29571
|
+
help="Optional Clawdad project slug used with --with-clawdad",
|
|
29572
|
+
)
|
|
29573
|
+
s_init.add_argument(
|
|
29574
|
+
"--clawdad-description",
|
|
29575
|
+
default="",
|
|
29576
|
+
help="Optional Clawdad project description used with --with-clawdad",
|
|
29577
|
+
)
|
|
29578
|
+
s_init.add_argument(
|
|
29579
|
+
"--current-codex",
|
|
29580
|
+
action="store_true",
|
|
29581
|
+
help="Save the current CODEX_THREAD_ID as the Codex resume target when tracking workspace/Clawdad startup state",
|
|
29582
|
+
)
|
|
29583
|
+
s_init.add_argument(
|
|
29584
|
+
"--codex-session-id",
|
|
29585
|
+
default="",
|
|
29586
|
+
help="Explicit Codex session id to save in workspace/Clawdad startup state",
|
|
29587
|
+
)
|
|
29588
|
+
s_init.add_argument(
|
|
29589
|
+
"--startup-dry-run",
|
|
29590
|
+
action="store_true",
|
|
29591
|
+
help="Plan external startup actions without running gh, orp workspace, or clawdad commands",
|
|
29592
|
+
)
|
|
28474
29593
|
s_init.add_argument(
|
|
28475
29594
|
"--json",
|
|
28476
29595
|
dest="json_output",
|