@cognisphere-sh/cognisphere-harness 0.12.0 → 0.13.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/CHANGELOG.md +66 -11
- package/LICENSE +21 -0
- package/dist-web/assets/{file-editor-CQSmlsy_.js → file-editor-CqGYKmT3.js} +1 -1
- package/dist-web/assets/{index-1Hg12QW0.js → index-CDE-jsH8.js} +3 -3
- package/dist-web/index.html +1 -1
- package/home-template/config.example +0 -6
- package/home-template/docs/base-harness/README.md +11 -4
- package/home-template/scripts/build.sh +0 -5
- package/home-template/scripts/lib/remote-bootstrap.sh +2 -6
- package/home-template/scripts/setup-server.sh +7 -39
- package/package.json +7 -5
- package/skills/cognisphere-upgrade/SKILL.md +1 -1
- package/skills/create-plugin/SKILL.md +2 -2
- package/src/agents/base-agent/bootstrap/bootstrap.sh +2 -18
- package/src/agents/base-agent/extensions/context-meta.ts +4 -1
- package/src/agents/base-agent/system_prompts/0-base_prompt.md +3 -0
- package/src/api/agents.ts +3 -13
- package/src/api/credentials.ts +0 -15
- package/src/api/models.ts +2 -8
- package/src/api/secrets.ts +3 -3
- package/src/cli/index.ts +1 -1
- package/src/cli/init.ts +0 -12
- package/src/core/agent-manager.ts +12 -47
- package/src/core/config.ts +6 -0
- package/src/core/main.ts +10 -2
- package/src/core/model-access.ts +55 -0
- package/src/core/models-catalog.ts +72 -40
- package/src/core/models-store.ts +3 -4
- package/src/core/oauth-logins.ts +7 -3
- package/src/core/pi-delivery-receipts.ts +69 -0
- package/src/core/pi-models-sync.ts +2 -2
- package/src/core/queue.ts +7 -25
- package/src/core/rpc.ts +55 -49
- package/src/core/runner.ts +137 -131
- package/src/core/secrets.ts +2 -3
- package/src/core/types.ts +1 -1
- package/src/plugins/artifacts/index.ts +1 -1
- package/src/agents/base-agent/extensions/harness-bridge.ts +0 -63
package/dist-web/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (_) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-CDE-jsH8.js"></script>
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/react-BIlrhC09.js">
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/assets/radix-DHW5y7jr.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/markdown-sumxz7Eq.js">
|
|
@@ -44,12 +44,6 @@ BACKUP_S3_ACCESS_KEY= # keys for that endpoint (blank = aws CLI chain / IAM r
|
|
|
44
44
|
BACKUP_S3_SECRET_KEY=
|
|
45
45
|
AWS_PROFILE= # local testing only: named aws-cli profile (EC2 uses the IAM role)
|
|
46
46
|
|
|
47
|
-
# Token for the private @cognisphere-sh/* packages on GitHub Packages.
|
|
48
|
-
# Leave blank to use the run user's `gh` CLI token (it must have the
|
|
49
|
-
# `read:packages` scope: gh auth refresh -s read:packages). Or paste a
|
|
50
|
-
# GitHub PAT with read:packages here for a non-interactive box.
|
|
51
|
-
COGNISPHERE_NPM_TOKEN=
|
|
52
|
-
|
|
53
47
|
# ---- deployment-specific params ---------------------------------------------
|
|
54
48
|
# This file is harness-owned (refreshed on upgrade). Your app's extra keys go
|
|
55
49
|
# in `config` too, but document them in scripts/app/config.example and read
|
|
@@ -95,8 +95,9 @@ plus an optional `X-App-User` header (see `app/README.md`).
|
|
|
95
95
|
agent (`agent/` scope is yours; `<plugin-id>/` scopes are seeded by
|
|
96
96
|
plugins and overwritten on every start).
|
|
97
97
|
- `bootstrap/bootstrap.sh` — runs on every agent start; provisions the
|
|
98
|
-
agent's `.venv`, system deps
|
|
99
|
-
|
|
98
|
+
agent's `.venv`, system deps and `agent-browser`. Pi itself isn't installed
|
|
99
|
+
here: the harness runs the `pi` bundled with its own package, at the version
|
|
100
|
+
it was tested with.
|
|
100
101
|
|
|
101
102
|
A **thread** is one conversation. `threadIdStrategy` controls how inbound
|
|
102
103
|
messages map to threads (one global thread, one per plugin, or one per
|
|
@@ -142,8 +143,14 @@ Both under `harness/.secrets/` (0600, gitignored, editable via the console):
|
|
|
142
143
|
- `secrets.json` — per-agent buckets: `agent` (agent-level env) plus one per
|
|
143
144
|
plugin id. All keys are injected into the agent's environment.
|
|
144
145
|
- `models.json` — per-provider credentials + the allowlist of enabled model
|
|
145
|
-
ids. Subscription OAuth logins (Claude Pro/Max,
|
|
146
|
-
the
|
|
146
|
+
ids. Subscription OAuth logins (Claude Pro/Max, Sign in with ChatGPT on
|
|
147
|
+
OpenAI, and the legacy OpenAI Codex) are connected from
|
|
148
|
+
the console's Models page instead of pasting keys. Their tokens live in
|
|
149
|
+
`.secrets/pi/` (this harness's own Pi config folder, not `~/.pi/agent`).
|
|
150
|
+
A model is usable when
|
|
151
|
+
its provider has its keys or a connected sign-in and the model is enabled.
|
|
152
|
+
A thread's own model (set from the chat header) that stops being usable
|
|
153
|
+
falls back to the agent's model until it is usable again.
|
|
147
154
|
- `gws/` — Google sign-in for agents with the **gws** plugin. The shared
|
|
148
155
|
"Web application" OAuth client (from your GCP project, with
|
|
149
156
|
`<console origin>/api/gws/oauth/callback` as a redirect URI) is saved
|
|
@@ -13,11 +13,6 @@ set -euo pipefail
|
|
|
13
13
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
14
14
|
[[ -f "$ROOT/config" ]] && source "$ROOT/config"
|
|
15
15
|
|
|
16
|
-
# @cognisphere-sh/* need a read:packages token even though they're public.
|
|
17
|
-
# ~/.npmrc references ${COGNISPHERE_NPM_TOKEN} (written by setup-server.sh);
|
|
18
|
-
# export it from config, falling back to the gh CLI token.
|
|
19
|
-
export COGNISPHERE_NPM_TOKEN="${COGNISPHERE_NPM_TOKEN:-$(gh auth token 2>/dev/null || true)}"
|
|
20
|
-
|
|
21
16
|
cd "$ROOT"
|
|
22
17
|
pnpm install --frozen-lockfile
|
|
23
18
|
if [[ -f "$ROOT/app/package.json" ]]; then
|
|
@@ -58,12 +58,8 @@ command -v claude >/dev/null || curl -fsSL https://claude.ai/install.sh | bash
|
|
|
58
58
|
grep -q '\.local/bin' ~/.bashrc || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
|
59
59
|
export PATH="$HOME/.local/bin:$PATH"
|
|
60
60
|
|
|
61
|
-
# gh interactive auth —
|
|
62
|
-
|
|
63
|
-
gh auth login -s read:packages
|
|
64
|
-
elif ! gh auth status 2>&1 | grep -q read:packages; then
|
|
65
|
-
gh auth refresh -s read:packages
|
|
66
|
-
fi
|
|
61
|
+
# gh interactive auth — used to clone the repo below
|
|
62
|
+
gh auth status >/dev/null 2>&1 || gh auth login
|
|
67
63
|
|
|
68
64
|
# Claude Code auth only — setup-token runs the OAuth flow and stores the
|
|
69
65
|
# credential without starting a session. Skipped if already authenticated.
|
|
@@ -50,33 +50,7 @@ npm install -g corepack@latest
|
|
|
50
50
|
corepack enable
|
|
51
51
|
corepack prepare pnpm@latest --activate
|
|
52
52
|
|
|
53
|
-
# ---- GitHub Packages token --------------------------------------------------
|
|
54
|
-
# @cognisphere-sh/* live on GitHub Packages, whose npm registry requires a token
|
|
55
|
-
# with the read:packages scope EVEN for public packages (anonymous = 401, a
|
|
56
|
-
# scope-short token = 403). Prefer COGNISPHERE_NPM_TOKEN from config (a PAT); else
|
|
57
|
-
# use the run user's gh CLI token. A gh OAuth token (gho_*) exists but often lacks
|
|
58
|
-
# read:packages, so a bare `gh auth token` check would pass yet still 403 at fetch
|
|
59
|
-
# time — verify/repair the scope here instead of failing mid-build.
|
|
60
|
-
if [[ -z "${COGNISPHERE_NPM_TOKEN:-}" ]]; then
|
|
61
|
-
command -v gh >/dev/null || { echo "no COGNISPHERE_NPM_TOKEN in config and gh CLI not installed. Set COGNISPHERE_NPM_TOKEN (a PAT with read:packages) in config, or install gh and run: sudo -u $RUN_USER gh auth login -s read:packages"; exit 1; }
|
|
62
|
-
# gh runs as the app user; ensure it's logged in and the token carries read:packages.
|
|
63
|
-
# (gh prints "Token scopes:" to stderr, hence 2>&1.)
|
|
64
|
-
if ! sudo -u "$RUN_USER" gh auth status >/dev/null 2>&1; then
|
|
65
|
-
echo ">> gh is not authenticated for $RUN_USER — launching login (needs read:packages)"
|
|
66
|
-
sudo -u "$RUN_USER" gh auth login -s read:packages || { echo "gh auth login failed"; exit 1; }
|
|
67
|
-
elif ! sudo -u "$RUN_USER" gh auth status 2>&1 | grep -q 'read:packages'; then
|
|
68
|
-
echo ">> gh token for $RUN_USER lacks the read:packages scope — refreshing"
|
|
69
|
-
sudo -u "$RUN_USER" gh auth refresh -s read:packages || { echo "gh auth refresh failed"; exit 1; }
|
|
70
|
-
fi
|
|
71
|
-
COGNISPHERE_NPM_TOKEN="$(sudo -u "$RUN_USER" gh auth token 2>/dev/null || true)"
|
|
72
|
-
fi
|
|
73
|
-
[[ -n "$COGNISPHERE_NPM_TOKEN" ]] || { echo "no GitHub token for @cognisphere-sh packages. Set COGNISPHERE_NPM_TOKEN in config (a PAT with read:packages), or run: sudo -u $RUN_USER gh auth refresh -s read:packages"; exit 1; }
|
|
74
|
-
# pnpm refuses to expand env-var credentials from the committed project .npmrc,
|
|
75
|
-
# so the auth line must live in the run user's ~/.npmrc (a trusted source).
|
|
76
53
|
RUN_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
|
|
77
|
-
sudo -u "$RUN_USER" touch "$RUN_HOME/.npmrc"
|
|
78
|
-
sudo -u "$RUN_USER" grep -qF 'npm.pkg.github.com/:_authToken' "$RUN_HOME/.npmrc" \
|
|
79
|
-
|| echo '//npm.pkg.github.com/:_authToken=${COGNISPHERE_NPM_TOKEN}' | sudo -u "$RUN_USER" tee -a "$RUN_HOME/.npmrc" >/dev/null
|
|
80
54
|
|
|
81
55
|
# ---- credentials ------------------------------------------------------------
|
|
82
56
|
# `scripts/server.sh secrets` is the single source: it writes users.json
|
|
@@ -89,12 +63,13 @@ sudo -u "$RUN_USER" bash "$ROOT/scripts/server.sh" secrets
|
|
|
89
63
|
# ---- build (as the app user so node_modules aren't root-owned) -------------
|
|
90
64
|
# Delegated to scripts/build.sh — the same command used for every later deploy
|
|
91
65
|
# (git pull && sudo ./scripts/server.sh restart).
|
|
92
|
-
sudo -u "$RUN_USER"
|
|
66
|
+
sudo -u "$RUN_USER" bash "$ROOT/scripts/build.sh"
|
|
93
67
|
|
|
94
68
|
# ---- agent runtimes: build each agent's Python venv + CLIs ------------------
|
|
95
69
|
# Each agent has a bootstrap/bootstrap.sh (idempotent) that creates <agent>/.venv
|
|
96
|
-
# with markitdown/ddgs and installs the
|
|
97
|
-
#
|
|
70
|
+
# with markitdown/ddgs and installs the agent-browser CLI agents call by name.
|
|
71
|
+
# (The harness runs the `pi` bundled with its own dependencies, not a global
|
|
72
|
+
# one.) Run as the app user so the venv and ~/.local installs aren't
|
|
98
73
|
# root-owned. Best-effort: a warning here shouldn't abort the whole provision.
|
|
99
74
|
for bs in "$ROOT"/harness/agents/*/bootstrap/bootstrap.sh; do
|
|
100
75
|
[[ -f "$bs" ]] || continue
|
|
@@ -114,13 +89,11 @@ NODE_BIN_DIR="$(dirname "$(command -v node)")"
|
|
|
114
89
|
# $NODE_BIN_DIR/pnpm.
|
|
115
90
|
PNPM_BIN="$(command -v pnpm)"
|
|
116
91
|
PNPM_BIN_DIR="$(dirname "$PNPM_BIN")"
|
|
117
|
-
#
|
|
118
|
-
# per-user install ($RUN_HOME/.local/bin/pi), so that dir MUST be on the unit
|
|
119
|
-
# PATH or every spawn fails with `spawn pi ENOENT`. node/pnpm may both live in
|
|
92
|
+
# $RUN_HOME/.local/bin holds per-user tool installs; node/pnpm may both live in
|
|
120
93
|
# /usr/bin, so don't rely on their dirs to carry it.
|
|
121
94
|
# $RUN_HOME/.npm-global/bin carries the npm-global CLIs the agent bootstrap
|
|
122
|
-
# installs as the app user (
|
|
123
|
-
# root-owned. Must be on the unit PATH or bare `agent-browser
|
|
95
|
+
# installs as the app user (agent-browser) when the default npm prefix is
|
|
96
|
+
# root-owned. Must be on the unit PATH or bare `agent-browser` spawns 127.
|
|
124
97
|
UNIT_PATH="$RUN_HOME/.local/bin:$RUN_HOME/.npm-global/bin:$PNPM_BIN_DIR:$NODE_BIN_DIR:/usr/bin:/bin"
|
|
125
98
|
|
|
126
99
|
# Renaming APP_NAME leaves the old name's units/nginx site/cron behind — clean those up by hand.
|
|
@@ -134,10 +107,6 @@ Type=simple
|
|
|
134
107
|
User=$RUN_USER
|
|
135
108
|
WorkingDirectory=$ROOT/harness
|
|
136
109
|
Environment=PATH=$UNIT_PATH
|
|
137
|
-
# Empty so pnpm can expand the \${COGNISPHERE_NPM_TOKEN} ref in the run user's
|
|
138
|
-
# ~/.npmrc without warning. The service does no installs, so no real token is
|
|
139
|
-
# needed at runtime.
|
|
140
|
-
Environment=COGNISPHERE_NPM_TOKEN=
|
|
141
110
|
ExecStart=$PNPM_BIN exec cognisphere serve --port $HARNESS_PORT
|
|
142
111
|
Restart=on-failure
|
|
143
112
|
RestartSec=3
|
|
@@ -162,7 +131,6 @@ Environment=PATH=$UNIT_PATH
|
|
|
162
131
|
Environment=NODE_ENV=production
|
|
163
132
|
Environment=PORT=$APP_PORT
|
|
164
133
|
Environment=HARNESS_URL=http://127.0.0.1:$HARNESS_PORT
|
|
165
|
-
Environment=COGNISPHERE_NPM_TOKEN=
|
|
166
134
|
# 'next start' honors \$PORT; passing "-- -p" trips Next 16's arg parser (it
|
|
167
135
|
# reads -p as a project dir), so rely on the PORT env above.
|
|
168
136
|
ExecStart=$PNPM_BIN start
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cognisphere-sh/cognisphere-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
@@ -16,17 +17,18 @@
|
|
|
16
17
|
"home-template",
|
|
17
18
|
"dist-web",
|
|
18
19
|
"skills",
|
|
19
|
-
"CHANGELOG.md"
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
20
22
|
],
|
|
21
23
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
24
|
+
"node": ">=22.19.0"
|
|
23
25
|
},
|
|
24
26
|
"publishConfig": {
|
|
25
27
|
"access": "public"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": "
|
|
29
|
-
"@earendil-works/pi-coding-agent": "
|
|
30
|
+
"@earendil-works/pi-ai": "0.99.2",
|
|
31
|
+
"@earendil-works/pi-coding-agent": "0.99.2",
|
|
30
32
|
"@hono/node-server": "^1.13.7",
|
|
31
33
|
"ajv": "^8.17.1",
|
|
32
34
|
"better-sqlite3": "^11.7.0",
|
|
@@ -9,7 +9,7 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
# CogniSphere Upgrade
|
|
11
11
|
|
|
12
|
-
Drive the **two-phase upgrade** (see `docs/
|
|
12
|
+
Drive the **two-phase upgrade** (see `docs/low-level/cli.md#upgrades`):
|
|
13
13
|
|
|
14
14
|
1. **Code** — bump the installed dependency (`pnpm`/the CLI does this).
|
|
15
15
|
2. **Data** — *this skill*: edit the harness dir (agents, plugins, secrets) to
|
|
@@ -204,8 +204,8 @@ Config/secret edits need an agent restart to take effect:
|
|
|
204
204
|
|
|
205
205
|
## Reference
|
|
206
206
|
|
|
207
|
-
-
|
|
208
|
-
|
|
207
|
+
- Plugin lifecycle: `docs/low-level/plugins.md#discovery-and-lifecycle`; on-disk layout:
|
|
208
|
+
`docs/low-level/agents.md#persistent-layout` (repository references).
|
|
209
209
|
- Builtin plugins are the best examples: `telegram` (long-poll + CLI +
|
|
210
210
|
multi-kind `EventType`), `gws` (poller + shared lib in seed),
|
|
211
211
|
`scheduler` (cron + CLI), `agent-messaging` (webhook inbox with
|
|
@@ -102,7 +102,7 @@ python -m pip install -r bootstrap/requirements.txt
|
|
|
102
102
|
# ── npm global prefix: make it user-writable ──────────────────────────
|
|
103
103
|
# The default npm prefix may be a root-owned dir (e.g. /usr), so `npm install
|
|
104
104
|
# -g` fails with EACCES when bootstrap runs as the (non-root) app user. Point
|
|
105
|
-
# it at ~/.npm-global — where the
|
|
105
|
+
# it at ~/.npm-global — where the agent-browser wrapper already looks — and
|
|
106
106
|
# put its bin dir on PATH for the rest of this script and this shell.
|
|
107
107
|
if command -v npm >/dev/null 2>&1; then
|
|
108
108
|
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
|
|
@@ -110,23 +110,7 @@ if command -v npm >/dev/null 2>&1; then
|
|
|
110
110
|
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
|
111
111
|
fi
|
|
112
112
|
|
|
113
|
-
# ── 3.
|
|
114
|
-
# Provides the `pi` binary that the harness runner spawns (`pi --mode rpc`).
|
|
115
|
-
# Pinned to the version the harness was tested against — keep in sync with
|
|
116
|
-
# `@earendil-works/pi-coding-agent` in packages/harness/package.json.
|
|
117
|
-
PI_VERSION="${PI_VERSION:-0.84.4}"
|
|
118
|
-
installed_pi="$(pi --version 2>/dev/null || true)"
|
|
119
|
-
if [ "$installed_pi" = "$PI_VERSION" ]; then
|
|
120
|
-
log "pi-coding-agent $PI_VERSION already installed ($(command -v pi))"
|
|
121
|
-
elif command -v npm >/dev/null 2>&1; then
|
|
122
|
-
log "installing @earendil-works/pi-coding-agent@$PI_VERSION via npm (found: ${installed_pi:-none})..."
|
|
123
|
-
npm install -g "@earendil-works/pi-coding-agent@$PI_VERSION" \
|
|
124
|
-
|| warn "npm install -g @earendil-works/pi-coding-agent@$PI_VERSION failed"
|
|
125
|
-
else
|
|
126
|
-
warn "npm not found; install Node.js (https://nodejs.org) and re-run, or install @earendil-works/pi-coding-agent@$PI_VERSION by hand"
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
# ── 4. agent-browser (npm global) + its Chrome build ──────────────────
|
|
113
|
+
# ── 3. agent-browser (npm global) + its Chrome build ──────────────────
|
|
130
114
|
if command -v npm >/dev/null 2>&1; then
|
|
131
115
|
if command -v agent-browser >/dev/null 2>&1; then
|
|
132
116
|
log "agent-browser already installed ($(command -v agent-browser))"
|
|
@@ -171,7 +171,10 @@ export default function contextMeta(pi: ExtensionAPI): void {
|
|
|
171
171
|
|
|
172
172
|
const last = event.messages[event.messages.length - 1];
|
|
173
173
|
if (!last) return;
|
|
174
|
-
|
|
174
|
+
// The last message is usually our own checkpoint (role "custom"): one is
|
|
175
|
+
// placed before every LLM call. Custom messages reach the model as user
|
|
176
|
+
// text, so the block goes there like on a plain user message.
|
|
177
|
+
if (last.role === "user" || last.role === "custom") {
|
|
175
178
|
if (typeof last.content === "string") last.content = `${last.content}\n\n${block}`;
|
|
176
179
|
else last.content.push({ type: "text", text: block });
|
|
177
180
|
} else if (last.role === "toolResult") {
|
|
@@ -136,6 +136,7 @@ Timestamp: Fri 2026-04-17 14:30:05 IST
|
|
|
136
136
|
Plugin: telegram
|
|
137
137
|
Channel: 12345
|
|
138
138
|
ThreadId: telegram-12345
|
|
139
|
+
EventId: 42
|
|
139
140
|
[IsSilent: true]
|
|
140
141
|
[Retry: true]
|
|
141
142
|
[Continuation: true]
|
|
@@ -154,6 +155,8 @@ ThreadId: telegram-12345
|
|
|
154
155
|
so the result routes back here. It is a harness routing id — distinct from
|
|
155
156
|
plugin-side ids like a Telegram chat id or Gmail thread id, which appear in
|
|
156
157
|
the plugin-contributed fields.
|
|
158
|
+
- **EventId** — the harness's id for this message, used to track its delivery.
|
|
159
|
+
You can ignore it.
|
|
157
160
|
- **IsSilent: true** — appears only on silent messages (background updates).
|
|
158
161
|
Do not act on a silent message alone; treat it as ambient context.
|
|
159
162
|
- **Retry: true** — a previous delivery of _this_ message failed or was
|
package/src/api/agents.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { agentDir, secretsRoot } from "../core/config.js";
|
|
|
18
18
|
import type { ServerConfig } from "../core/config.js";
|
|
19
19
|
import { ModelsStore } from "../core/models-store.js";
|
|
20
20
|
import { findProviderInCatalog } from "../core/models-catalog.js";
|
|
21
|
-
import {
|
|
21
|
+
import { modelUnavailableReason } from "../core/model-access.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* /api/agents/* surface for the web UI.
|
|
@@ -284,18 +284,8 @@ export function agentsRouter(am: AgentManager, cfg: ServerConfig): Hono {
|
|
|
284
284
|
|
|
285
285
|
const entry = findProviderInCatalog(provider);
|
|
286
286
|
if (!entry) return c.json({ error: `unknown provider "${provider}"` }, 400);
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
!!pcfg && requiredCredentialsPresent(entry.credentials, pcfg.credentials);
|
|
290
|
-
if (!configured) {
|
|
291
|
-
return c.json({ error: `provider "${provider}" is not configured` }, 400);
|
|
292
|
-
}
|
|
293
|
-
if (!pcfg.enabledModels.includes(modelId)) {
|
|
294
|
-
return c.json(
|
|
295
|
-
{ error: `model "${modelId}" is not enabled for ${provider}` },
|
|
296
|
-
400,
|
|
297
|
-
);
|
|
298
|
-
}
|
|
287
|
+
const unavailable = modelUnavailableReason(models, provider, modelId);
|
|
288
|
+
if (unavailable) return c.json({ error: unavailable }, 400);
|
|
299
289
|
if (thinkingLevel !== null && !THINKING_LEVELS.has(thinkingLevel)) {
|
|
300
290
|
return c.json({ error: "invalid thinkingLevel" }, 400);
|
|
301
291
|
}
|
package/src/api/credentials.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { CredField } from "../core/types.js";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Shared credential masking/merge helpers for /api/models and /api/secrets.
|
|
5
3
|
*
|
|
@@ -27,16 +25,3 @@ export function applyMaskedPut(
|
|
|
27
25
|
// unchanged — keep existing value
|
|
28
26
|
} else if (typeof v === "string") target[key] = v;
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
/** Every required credential field has a non-empty stored value. */
|
|
32
|
-
export function requiredCredentialsPresent(
|
|
33
|
-
fields: CredField[],
|
|
34
|
-
stored: Record<string, string>,
|
|
35
|
-
): boolean {
|
|
36
|
-
return fields
|
|
37
|
-
.filter((f) => f.required)
|
|
38
|
-
.every((f) => {
|
|
39
|
-
const v = stored[f.key];
|
|
40
|
-
return typeof v === "string" && v.length > 0;
|
|
41
|
-
});
|
|
42
|
-
}
|
package/src/api/models.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ServerConfig } from "../core/config.js";
|
|
|
6
6
|
import { secretsRoot } from "../core/config.js";
|
|
7
7
|
import type { Logger } from "../core/logger.js";
|
|
8
8
|
import { PROVIDER_CATALOG } from "../core/models-catalog.js";
|
|
9
|
+
import { providerAuthenticated } from "../core/model-access.js";
|
|
9
10
|
import { ModelsStore } from "../core/models-store.js";
|
|
10
11
|
import { OAuthLoginManager } from "../core/oauth-logins.js";
|
|
11
12
|
import { syncPiModelOverrides } from "../core/pi-models-sync.js";
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
applyMaskedPut,
|
|
15
16
|
MASK,
|
|
16
17
|
maskCredential,
|
|
17
|
-
requiredCredentialsPresent,
|
|
18
18
|
} from "./credentials.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -124,16 +124,10 @@ export function modelsRouter(
|
|
|
124
124
|
for (const field of entry.credentials) {
|
|
125
125
|
values[field.key] = maskCredential(stored[field.key], field.secret);
|
|
126
126
|
}
|
|
127
|
-
const requiredOk = requiredCredentialsPresent(entry.credentials, stored);
|
|
128
127
|
const oauthConnected =
|
|
129
128
|
entry.oauth === true &&
|
|
130
129
|
readStoredCredential(entry.id)?.type === "oauth";
|
|
131
|
-
|
|
132
|
-
// otherwise OAuth connection satisfies missing required fields.
|
|
133
|
-
const configured =
|
|
134
|
-
entry.credentials.length === 0
|
|
135
|
-
? oauthConnected
|
|
136
|
-
: requiredOk || oauthConnected;
|
|
130
|
+
const configured = providerAuthenticated(entry, stored);
|
|
137
131
|
return {
|
|
138
132
|
id: entry.id,
|
|
139
133
|
displayName: entry.displayName,
|
package/src/api/secrets.ts
CHANGED
|
@@ -26,9 +26,9 @@ import { applyMaskedPut, MASK, maskCredential } from "./credentials.js";
|
|
|
26
26
|
* secrets[agentId][bucketId][KEY] = maskedString
|
|
27
27
|
* schemas[agentId][bucketId] = JsonSchema
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Stores plaintext on disk and exposes resolved values to pi as env vars.
|
|
30
|
+
* PUT invalidates the cache and requests a soft reload of affected agents;
|
|
31
|
+
* active batches drain before replacement runners use the new values.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
type Bucket = Record<string, string>;
|
package/src/cli/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `cognisphere` CLI entrypoint — install, scaffold, run, and upgrade a harness
|
|
3
|
-
* (see docs/
|
|
3
|
+
* (see docs/low-level/cli.md#cli). Dispatch only; each command
|
|
4
4
|
* lives in its own module.
|
|
5
5
|
*/
|
|
6
6
|
import { cmdAgentNew } from "./agent.js";
|
package/src/cli/init.ts
CHANGED
|
@@ -35,14 +35,6 @@ config
|
|
|
35
35
|
**/inboxes/
|
|
36
36
|
`;
|
|
37
37
|
|
|
38
|
-
// The auth token deliberately is NOT here: pnpm refuses to expand env-var
|
|
39
|
-
// credentials from a committed project .npmrc, so the _authToken line must
|
|
40
|
-
// live in the run user's ~/.npmrc (scripts/setup-server.sh writes it on the
|
|
41
|
-
// server; `init` prints the one-liner for local installs).
|
|
42
|
-
const NPMRC = `# @cognisphere-sh/* packages live on GitHub Packages, not npmjs.org.
|
|
43
|
-
@cognisphere-sh:registry=https://npm.pkg.github.com
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
38
|
const WORKSPACE_YAML = `packages:
|
|
47
39
|
- harness
|
|
48
40
|
- app
|
|
@@ -79,7 +71,6 @@ export function cmdInit(argv: string[]): void {
|
|
|
79
71
|
description: `${id}: cognisphere app home — the agent harness (harness/) + the user-facing app (app/). Workspace members are listed in pnpm-workspace.yaml.`,
|
|
80
72
|
});
|
|
81
73
|
writeFileSync(join(dir, "pnpm-workspace.yaml"), WORKSPACE_YAML);
|
|
82
|
-
writeFileSync(join(dir, ".npmrc"), NPMRC);
|
|
83
74
|
writeFileSync(join(dir, ".gitignore"), GITIGNORE);
|
|
84
75
|
|
|
85
76
|
// Deploy scripts, config templates, and the app/ placeholder — shipped with
|
|
@@ -148,9 +139,6 @@ export function cmdInit(argv: string[]): void {
|
|
|
148
139
|
info("");
|
|
149
140
|
info("Next steps:");
|
|
150
141
|
info(` cd ${cdTarget}`);
|
|
151
|
-
info(" # the private registry needs a read:packages token in YOUR ~/.npmrc");
|
|
152
|
-
info(" # (same env var the deploy scripts use — export it before installing):");
|
|
153
|
-
info(" echo '//npm.pkg.github.com/:_authToken=${COGNISPHERE_NPM_TOKEN}' >> ~/.npmrc");
|
|
154
142
|
info(" pnpm install");
|
|
155
143
|
info(" cd harness");
|
|
156
144
|
info(" pnpm exec cognisphere agent new <name> # add your first agent");
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { Ajv, type ErrorObject } from "ajv";
|
|
6
5
|
import type { ServerConfig } from "./config.js";
|
|
7
6
|
import { agentDir, agentsRoot, secretsRoot } from "./config.js";
|
|
@@ -12,6 +11,7 @@ import { CORE_PLUGIN_IDS } from "./plugin-registry.js";
|
|
|
12
11
|
import { AgentDb } from "./queue.js";
|
|
13
12
|
import { AgentRunner } from "./runner.js";
|
|
14
13
|
import { findProviderInCatalog } from "./models-catalog.js";
|
|
14
|
+
import { modelUnavailableReason } from "./model-access.js";
|
|
15
15
|
import { ModelsStore } from "./models-store.js";
|
|
16
16
|
import { syncPiModelOverrides } from "./pi-models-sync.js";
|
|
17
17
|
import { AGENT_BUCKET, SecretsStore } from "./secrets.js";
|
|
@@ -70,10 +70,9 @@ export class LifecycleError extends Error {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Loads agents from disk on boot and exposes them by id.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* server picks them up at boot.
|
|
73
|
+
* Loads agents from disk on boot and exposes them by id. The CLI scaffolds
|
|
74
|
+
* agent directories; the server discovers them at boot. Settings APIs reload
|
|
75
|
+
* existing agents and plugins. See docs/low-level/core.md for operating procedures.
|
|
77
76
|
*
|
|
78
77
|
* Lifecycle:
|
|
79
78
|
* - boot() → load every agent dir; failed agents stay listed.
|
|
@@ -491,6 +490,8 @@ export class AgentManager {
|
|
|
491
490
|
envSecrets,
|
|
492
491
|
resolveProviderEnv: (providerId) =>
|
|
493
492
|
resolveProviderEnv(this.models, providerId, dir, this.log),
|
|
493
|
+
modelUnavailableReason: (providerId, modelId) =>
|
|
494
|
+
modelUnavailableReason(this.models, providerId, modelId),
|
|
494
495
|
log: childLogger(`agent:${inst.id}`),
|
|
495
496
|
});
|
|
496
497
|
// When a stale-reload is pending and the last active batch finishes,
|
|
@@ -770,10 +771,10 @@ const VERTEX_SA_FILE = ".vertex-sa.json";
|
|
|
770
771
|
|
|
771
772
|
/**
|
|
772
773
|
* Validate the agent's chosen provider/model against operator settings and
|
|
773
|
-
* return the env vars pi should see. Throws
|
|
774
|
-
* the model
|
|
775
|
-
*
|
|
776
|
-
*
|
|
774
|
+
* return the env vars pi should see. Throws with the shared
|
|
775
|
+
* `modelUnavailableReason()` when the model can't be used — but only when a
|
|
776
|
+
* model id is specified; an agent.json with provider but no model passes
|
|
777
|
+
* through silently (preserves the pre-merge permissive behavior).
|
|
777
778
|
*
|
|
778
779
|
* Special case: google-vertex's `serviceAccountKey` field is paste-blob
|
|
779
780
|
* JSON — we write it to <agentDir>/.vertex-sa.json (0600) and point
|
|
@@ -793,47 +794,11 @@ function resolveAndValidateProvider(
|
|
|
793
794
|
): Record<string, string> {
|
|
794
795
|
const providerId = agentJson.model?.provider;
|
|
795
796
|
if (!providerId) return {};
|
|
796
|
-
const entry = findProviderInCatalog(providerId);
|
|
797
|
-
if (!entry) return {};
|
|
798
|
-
// Subscription OAuth (tokens in pi's own auth.json) substitutes for
|
|
799
|
-
// models.json credentials — pi children resolve auth.json themselves,
|
|
800
|
-
// so nothing extra is injected; only the model allowlist still applies.
|
|
801
|
-
const oauthConnected =
|
|
802
|
-
entry.oauth === true && readStoredCredential(providerId)?.type === "oauth";
|
|
803
|
-
const cfg =
|
|
804
|
-
models.getProvider(providerId) ??
|
|
805
|
-
(oauthConnected ? { credentials: {}, enabledModels: [] } : undefined);
|
|
806
797
|
const modelId = agentJson.model?.id;
|
|
807
|
-
|
|
808
|
-
if (!cfg) {
|
|
809
|
-
if (modelId) {
|
|
810
|
-
throw new Error(
|
|
811
|
-
`agent ${agentId}: provider ${providerId} is not configured in Models settings`,
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
return {};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
798
|
if (modelId) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
`agent ${agentId}: model ${providerId}/${modelId} is not enabled in Models settings`,
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
const missing = entry.credentials
|
|
824
|
-
.filter((f) => f.required)
|
|
825
|
-
.filter((f) => {
|
|
826
|
-
const v = cfg.credentials[f.key];
|
|
827
|
-
return typeof v !== "string" || v.length === 0;
|
|
828
|
-
})
|
|
829
|
-
.map((f) => f.label);
|
|
830
|
-
if (missing.length > 0 && !oauthConnected) {
|
|
831
|
-
throw new Error(
|
|
832
|
-
`agent ${agentId}: provider ${providerId} is missing required credentials: ${missing.join(", ")} (set them in Models settings)`,
|
|
833
|
-
);
|
|
834
|
-
}
|
|
799
|
+
const reason = modelUnavailableReason(models, providerId, modelId);
|
|
800
|
+
if (reason) throw new Error(`agent ${agentId}: ${reason}`);
|
|
835
801
|
}
|
|
836
|
-
|
|
837
802
|
return resolveProviderEnv(models, providerId, agentDir, log);
|
|
838
803
|
}
|
|
839
804
|
|
package/src/core/config.ts
CHANGED
|
@@ -67,6 +67,12 @@ export function secretsRoot(cfg: ServerConfig): string {
|
|
|
67
67
|
return join(harnessRoot(cfg), ".secrets");
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Pi's config folder for this harness (`PI_CODING_AGENT_DIR`). Under
|
|
71
|
+
* `.secrets/` because its `auth.json` holds sign-in tokens. */
|
|
72
|
+
export function piAgentDir(cfg: ServerConfig): string {
|
|
73
|
+
return join(secretsRoot(cfg), "pi");
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
/** Path of the harness-wide settings file (currently just `{ timezone }`). */
|
|
71
77
|
export function harnessJsonFile(cfg: ServerConfig): string {
|
|
72
78
|
return join(harnessRoot(cfg), "harness.json");
|
package/src/core/main.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
|
|
|
2
2
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -23,7 +23,7 @@ import { modelsRouter } from "../api/models.js";
|
|
|
23
23
|
import { secretsRouter } from "../api/secrets.js";
|
|
24
24
|
import { maybeHandleWebhook } from "../api/webhook.js";
|
|
25
25
|
import { AgentManager } from "./agent-manager.js";
|
|
26
|
-
import { loadConfig, userPluginsRoot } from "./config.js";
|
|
26
|
+
import { loadConfig, piAgentDir, userPluginsRoot } from "./config.js";
|
|
27
27
|
import { childLogger, rootLogger } from "./logger.js";
|
|
28
28
|
import { PluginRegistry } from "./plugin-registry.js";
|
|
29
29
|
|
|
@@ -50,6 +50,14 @@ async function main(): Promise<void> {
|
|
|
50
50
|
const log = rootLogger();
|
|
51
51
|
log.info({ cfg }, "boot");
|
|
52
52
|
|
|
53
|
+
// Pi's config folder (auth.json sign-ins, models.json, settings.json,
|
|
54
|
+
// trust.json) is per harness, not the run user's global ~/.pi/agent, so
|
|
55
|
+
// harnesses and the operator's own `pi` never share or overwrite each
|
|
56
|
+
// other's settings. Pi reads the variable on every lookup; every pi child
|
|
57
|
+
// inherits it via `{...process.env}` (runner.spawnPi).
|
|
58
|
+
process.env.PI_CODING_AGENT_DIR = piAgentDir(cfg);
|
|
59
|
+
mkdirSync(piAgentDir(cfg), { recursive: true, mode: 0o700 });
|
|
60
|
+
|
|
53
61
|
// Shared secret that authenticates in-harness callers of the agent-messaging
|
|
54
62
|
// webhook. Generated per boot unless the operator pins one; inherited by
|
|
55
63
|
// every pi child via `{...process.env}` (runner.spawnPi) so seeded scripts
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { findProviderInCatalog } from "./models-catalog.js";
|
|
3
|
+
import type { ModelsStore } from "./models-store.js";
|
|
4
|
+
import type { CredField, ProviderCatalogEntry } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** Every required credential field has a non-empty stored value. */
|
|
7
|
+
export function requiredCredentialsPresent(
|
|
8
|
+
fields: CredField[],
|
|
9
|
+
stored: Record<string, string>,
|
|
10
|
+
): boolean {
|
|
11
|
+
return fields
|
|
12
|
+
.filter((f) => f.required)
|
|
13
|
+
.every((f) => {
|
|
14
|
+
const v = stored[f.key];
|
|
15
|
+
return typeof v === "string" && v.length > 0;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A provider can authenticate when its required credentials are stored or a
|
|
21
|
+
* subscription sign-in is connected (tokens live in pi's own auth.json).
|
|
22
|
+
* OAuth-only providers (no credential fields) need the sign-in.
|
|
23
|
+
*/
|
|
24
|
+
export function providerAuthenticated(
|
|
25
|
+
entry: ProviderCatalogEntry,
|
|
26
|
+
stored: Record<string, string>,
|
|
27
|
+
): boolean {
|
|
28
|
+
const oauthConnected =
|
|
29
|
+
entry.oauth === true && readStoredCredential(entry.id)?.type === "oauth";
|
|
30
|
+
if (entry.credentials.length === 0) return oauthConnected;
|
|
31
|
+
return oauthConnected || requiredCredentialsPresent(entry.credentials, stored);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Why `providerId/modelId` can't be used right now, or null if it can. The one
|
|
36
|
+
* rule shared by agent startup, the thread-model endpoint and every batch
|
|
37
|
+
* spawn. Providers outside the catalog pass through: pi resolves them from the
|
|
38
|
+
* ambient environment.
|
|
39
|
+
*/
|
|
40
|
+
export function modelUnavailableReason(
|
|
41
|
+
models: ModelsStore,
|
|
42
|
+
providerId: string,
|
|
43
|
+
modelId: string,
|
|
44
|
+
): string | null {
|
|
45
|
+
const entry = findProviderInCatalog(providerId);
|
|
46
|
+
if (!entry) return null;
|
|
47
|
+
const cfg = models.getProvider(providerId);
|
|
48
|
+
if (!providerAuthenticated(entry, cfg?.credentials ?? {})) {
|
|
49
|
+
return `provider ${providerId} has no credentials or sign-in in Models settings`;
|
|
50
|
+
}
|
|
51
|
+
if (!cfg?.enabledModels.includes(modelId)) {
|
|
52
|
+
return `model ${providerId}/${modelId} is not enabled in Models settings`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|