@meffecta/agent 1.0.2 → 1.0.7

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.
@@ -32,12 +32,13 @@ set -euo pipefail
32
32
  # is restarted for them anyway. scripts/deploy.sh does it automatically.
33
33
  #
34
34
  # Usage:
35
- # scripts/setup-scheduler.sh # from the content repo (./deployment.env)
36
- # scripts/setup-scheduler.sh --dry-run # print what it would create, change nothing
35
+ # scripts/sync-triggers.sh # from the content repo (./deployment.env)
36
+ # scripts/sync-triggers.sh --dry-run # print what it would create, change nothing
37
37
  #
38
38
  # Which deployment: ./deployment.env, or --config <file>. See deployment.env.example.
39
39
 
40
40
  . "$(dirname "${BASH_SOURCE[0]}")/lib/deployment.sh"
41
+ . "$(dirname "${BASH_SOURCE[0]}")/lib/tasks-queue.sh"
41
42
 
42
43
  DRY_RUN=false
43
44
  QUEUE_NAME=""
@@ -70,8 +71,8 @@ GC="gcloud --project=${PROJECT}"
70
71
  # arguments of the calls that create them — and gcloud echoes the arguments back when it
71
72
  # rejects one. Everything this script prints goes through here first.
72
73
  redact() {
73
- if [ -n "${WEBHOOK_SECRET:-}" ]; then
74
- printf '%s\n' "${1//${WEBHOOK_SECRET}/<secret>}"
74
+ if [ -n "${API_SECRET:-}" ]; then
75
+ printf '%s\n' "${1//${API_SECRET}/<secret>}"
75
76
  else
76
77
  printf '%s\n' "$1"
77
78
  fi
@@ -119,18 +120,24 @@ fi
119
120
 
120
121
  # The triggers authenticate with the same bearer token as everything else on this API, so
121
122
  # the secret has to be resolved — it is normally a Secret Manager reference on the service.
122
- SECRET_REF=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_WEBHOOK_SECRET)
123
- IFS=$'\t' read -r SECRET_KIND SECRET_A SECRET_B <<<"${SECRET_REF}"
124
- if [ "${SECRET_KIND}" = "value" ]; then
125
- WEBHOOK_SECRET="${SECRET_A}"
126
- elif [ "${SECRET_KIND}" = "secret" ] && [ -n "${SECRET_A}" ]; then
127
- WEBHOOK_SECRET=$(${GC} secrets versions access "${SECRET_B}" --secret="${SECRET_A}")
128
- else
129
- echo "Could not resolve AGENT_WEBHOOK_SECRET from ${SERVICE} is it set on the service?" >&2
130
- echo " scripts/set-secret.sh AGENT_WEBHOOK_SECRET --random" >&2
123
+ # AGENT_WEBHOOK_SECRET is the old name, still read by the engine and still looked for here,
124
+ # so a deployment part-way through the rename gets working triggers either way.
125
+ API_SECRET=""
126
+ for SECRET_VAR in AGENT_API_SECRET AGENT_WEBHOOK_SECRET; do
127
+ IFS=$'\t' read -r SECRET_KIND SECRET_A SECRET_B <<<"$(printf '%s' "${SERVICE_JSON}" | svc_field "${SECRET_VAR}")"
128
+ if [ "${SECRET_KIND}" = "value" ]; then
129
+ API_SECRET="${SECRET_A}"
130
+ elif [ "${SECRET_KIND}" = "secret" ] && [ -n "${SECRET_A}" ]; then
131
+ API_SECRET=$(${GC} secrets versions access "${SECRET_B}" --secret="${SECRET_A}")
132
+ fi
133
+ [ -n "${API_SECRET}" ] && break
134
+ done
135
+ if [ -z "${API_SECRET}" ]; then
136
+ echo "Could not resolve AGENT_API_SECRET (or the old AGENT_WEBHOOK_SECRET) from ${SERVICE}." >&2
137
+ echo " $(cmd set-secret) AGENT_API_SECRET --random" >&2
138
+ echo "Refusing to create triggers that would 401 on every firing." >&2
131
139
  exit 1
132
140
  fi
133
- [ -n "${WEBHOOK_SECRET}" ] || { echo "AGENT_WEBHOOK_SECRET resolved empty — refusing to create triggers that would 401." >&2; exit 1; }
134
141
 
135
142
  echo " url: ${AGENT_URL}"
136
143
  echo " runtime: ${RUNTIME_SA}"
@@ -142,25 +149,20 @@ if [ "${MIN_SCALE}" != "0" ] || [ "${CPU_THROTTLING}" = "false" ]; then
142
149
  ⚠️ ${SERVICE} still runs always-on (min-instances=${MIN_SCALE}, cpu-throttling=${CPU_THROTTLING}).
143
150
  External triggers work either way, but the saving does not arrive until the service
144
151
  scales to zero — and until then in-process timers and Cloud Scheduler would BOTH fire,
145
- so every cron runs twice. The shape lives in deployment.env and deploy.sh asserts it:
152
+ so every cron runs twice. The shape lives in deployment.env, and is asserted on every
153
+ rollout:
146
154
 
147
155
  SCALING=scale-to-zero # in deployment.env, then
148
- scripts/deploy.sh # rolls it out (--dry-run to see the shape first)
156
+ $(cmd deploy) # rolls it out (--dry-run to see the shape first)
149
157
 
150
158
  EOF
151
159
  fi
152
160
 
161
+ # Normally already there — setup-infrastructure.sh creates it with the deployment's other
162
+ # static resources. Ensured here too for a deployment provisioned before that was true, and
163
+ # because this script is also run on its own. See lib/tasks-queue.sh.
153
164
  echo "📥 Cloud Tasks queue ${QUEUE}..."
154
- if ! ${GC} tasks queues describe "${QUEUE}" --location="${REGION}" >/dev/null 2>&1; then
155
- run ${GC} tasks queues create "${QUEUE}" --location="${REGION}" --quiet
156
- fi
157
- # max-concurrent-dispatches leaves room for several held attempts at once; the engine's own
158
- # queue is serial, so this bounds requests waiting, not jobs running.
159
- run ${GC} tasks queues update "${QUEUE}" --location="${REGION}" \
160
- --max-attempts=4 --min-backoff=5s --max-backoff=30s --max-doublings=2 \
161
- --max-concurrent-dispatches=8 --max-dispatches-per-second=2 --quiet
162
- run ${GC} tasks queues add-iam-policy-binding "${QUEUE}" --location="${REGION}" \
163
- --member="serviceAccount:${RUNTIME_SA}" --role="roles/cloudtasks.enqueuer" --quiet
165
+ ensure_tasks_queue "${QUEUE}"
164
166
 
165
167
  CURRENT_QUEUE=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_TASKS_QUEUE | cut -f2)
166
168
  CURRENT_URL=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_PUBLIC_URL | cut -f2)
@@ -171,7 +173,7 @@ if [ "${CURRENT_QUEUE}" != "${QUEUE_PATH}" ] || [ "${CURRENT_URL}" != "${AGENT_U
171
173
  fi
172
174
 
173
175
  echo "📋 Reading the job list from ${AGENT_URL}/jobs..."
174
- JOBS_JSON=$(curl -fsS --max-time 300 -H "Authorization: Bearer ${WEBHOOK_SECRET}" "${AGENT_URL}/jobs") || {
176
+ JOBS_JSON=$(curl -fsS --max-time 300 -H "Authorization: Bearer ${API_SECRET}" "${AGENT_URL}/jobs") || {
175
177
  cat >&2 <<EOF
176
178
  Could not read ${AGENT_URL}/jobs.
177
179
 
@@ -179,7 +181,7 @@ The engine is the only thing that knows which jobs exist — it reads them from
179
181
  clone of the content repo. So the service has to be deployed and healthy first:
180
182
 
181
183
  curl -fsS ${AGENT_URL}/health
182
- scripts/deploy.sh
184
+ $(cmd deploy)
183
185
 
184
186
  A first request after a scale-to-zero idle also has to wait out a cold start, which
185
187
  includes cloning the content repo.
@@ -193,7 +195,7 @@ EOF
193
195
  CRON_JOBS=$(printf '%s' "${JOBS_JSON}" | node "${READ_JSON}" crons)
194
196
  WATCHES_INBOX=$(printf '%s' "${JOBS_JSON}" | node "${READ_JSON}" watches-inbox)
195
197
 
196
- HEADERS="Content-Type=application/json,Authorization=Bearer ${WEBHOOK_SECRET}"
198
+ HEADERS="Content-Type=application/json,Authorization=Bearer ${API_SECRET}"
197
199
 
198
200
  # `create http` takes --headers; `update http` insists on --update-headers for the same
199
201
  # thing. Everything else about the two calls is identical.
@@ -30,6 +30,8 @@ set -euo pipefail
30
30
  # Artifact Registry). A deployment using an AR remote repository instead still needs it
31
31
  # here — once per engine version, rather than once per deploy.
32
32
 
33
+ . "$(dirname "${BASH_SOURCE[0]}")/lib/cli-names.sh"
34
+
33
35
  GHCR_IMAGE="${GHCR_IMAGE:-ghcr.io/meffecta/agent}"
34
36
  TAG="latest"
35
37
  IMAGE=""
@@ -114,5 +116,5 @@ fi
114
116
  # This script is one of the files being overwritten, and bash reads a script as it runs it.
115
117
  # exec hands the process over so nothing further is read from the file underneath us.
116
118
  echo "✅ Updated. Review with \`git diff\`, then deploy the matching engine:"
117
- echo " scripts/deploy.sh${TAG:+ --tag ${TAG}}"
119
+ echo " $(cmd deploy)${TAG:+ --tag ${TAG}}"
118
120
  exec cp -R "${TMP}/scripts/." "${DEST}/"
@@ -8,6 +8,8 @@
8
8
  // Never prints a credential value. Exits 1 if anything configured actually fails.
9
9
 
10
10
  import { execFile } from "node:child_process";
11
+ import { readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
11
13
 
12
14
  const env = process.env;
13
15
  const results = [];
@@ -45,12 +47,21 @@ async function metadataToken(scopes) {
45
47
 
46
48
  async function checkGithub() {
47
49
  if (!env.GITHUB_TOKEN) {
48
- return skip("GITHUB_TOKEN", "unset");
50
+ // Not a shrug: the service clones the content repo at boot, so unset is an outage.
51
+ return fail("GITHUB_TOKEN", "unset — the service cannot clone its content repo without it");
49
52
  }
50
- // The content repo (from GIT_REPO_URL, when it's a GitHub URL), plus the working
51
- // repos this deployment's jobs clone (deployment-specificderive from jobs/ one day).
53
+ // The content repo (from GIT_REPO_URL, when it is a GitHub URL), plus any working repo
54
+ // this deployment names in a *_GIT_URL variable its jobs clone those, so the token has
55
+ // to reach them. Nothing here may name a company: this script ships to every deployment.
52
56
  const contentRepo = env.GIT_REPO_URL?.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1];
53
- const repos = [...(contentRepo ? [contentRepo] : []), "meffecta/sweepos"];
57
+ const named = Object.entries(env)
58
+ .filter(([k]) => k.endsWith("_GIT_URL"))
59
+ .map(([, v]) => v?.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1])
60
+ .filter(Boolean);
61
+ const repos = [...new Set([...(contentRepo ? [contentRepo] : []), ...named])];
62
+ if (repos.length === 0) {
63
+ return skip("GITHUB_TOKEN", "set, but no GitHub repo to check it against");
64
+ }
54
65
  const codes = await Promise.all(
55
66
  repos.map(
56
67
  async (r) =>
@@ -87,35 +98,38 @@ async function checkResend() {
87
98
  : fail("RESEND_API_KEY", `domains → ${status}`);
88
99
  }
89
100
 
90
- async function checkInternalApi() {
91
- if (!env.SWEEPOS_API_URL || !env.INTERNAL_API_KEY) {
92
- return skip("SWEEPOS internal API", "SWEEPOS_API_URL/INTERNAL_API_KEY not set");
93
- }
94
- const { status } = await http(`${env.SWEEPOS_API_URL}/v1/internal/leads?limit=1`, {
95
- headers: { Authorization: `Bearer ${env.INTERNAL_API_KEY}` },
96
- });
97
- status === 200 ? ok("SWEEPOS internal API") : fail("SWEEPOS internal API", `leads ${status}`);
98
- }
99
-
101
+ /**
102
+ * Every read-only Postgres URL this deployment declares, by the documented convention
103
+ * <WORLD>_POSTGRES_URL_READONLY — the variable names belong to the deployment, so they are
104
+ * discovered rather than listed here.
105
+ */
106
+ // There is deliberately no check for a deployment's own product API. Probing one means
107
+ // knowing its path and its response shape — /v1/internal/leads was SweepOS's — and this
108
+ // script ships to every deployment. A credential the engine's own skills use is fair game
109
+ // here; a company's API is that company's tooling.
100
110
  function checkPostgres() {
101
- return new Promise((resolve) => {
102
- if (!env.SWEEPOS_POSTGRES_URL_READONLY) {
103
- skip("SWEEPOS_POSTGRES_URL_READONLY", "unset");
104
- return resolve();
105
- }
106
- execFile("psql", [env.SWEEPOS_POSTGRES_URL_READONLY, "-Atc", "SELECT 1"], { timeout: 10_000 }, (err, stdout) => {
107
- if (err) {
108
- err.code === "ENOENT"
109
- ? skip("SWEEPOS_POSTGRES_URL_READONLY", "psql not installed here")
110
- : fail("SWEEPOS_POSTGRES_URL_READONLY", err.message.split("\n")[0].slice(0, 120));
111
- } else {
112
- stdout.trim() === "1"
113
- ? ok("SWEEPOS_POSTGRES_URL_READONLY")
114
- : fail("SWEEPOS_POSTGRES_URL_READONLY", "unexpected result");
115
- }
116
- resolve();
117
- });
118
- });
111
+ const urls = Object.keys(env).filter((k) => k.endsWith("_POSTGRES_URL_READONLY") && env[k]);
112
+ if (urls.length === 0) {
113
+ skip("POSTGRES (read-only)", "no <WORLD>_POSTGRES_URL_READONLY set");
114
+ return Promise.resolve();
115
+ }
116
+ return Promise.all(
117
+ urls.map(
118
+ (name) =>
119
+ new Promise((resolve) => {
120
+ execFile("psql", [env[name], "-Atc", "SELECT 1"], { timeout: 10_000 }, (err, stdout) => {
121
+ if (err) {
122
+ err.code === "ENOENT"
123
+ ? skip(name, "psql not installed here")
124
+ : fail(name, err.message.split("\n")[0].slice(0, 120));
125
+ } else {
126
+ stdout.trim() === "1" ? ok(name) : fail(name, "unexpected result");
127
+ }
128
+ resolve();
129
+ });
130
+ }),
131
+ ),
132
+ );
119
133
  }
120
134
 
121
135
  async function checkPosthog() {
@@ -138,11 +152,13 @@ async function checkAhrefs() {
138
152
  if (!env.AHREFS_API_KEY) {
139
153
  return skip("AHREFS_API_KEY", "unset");
140
154
  }
141
- const { status, json } = await http("https://api.ahrefs.com/v3/public/domain-rating-free?target=sweepos.app", {
155
+ // example.com rather than a domain belonging to any deployment: this only proves the key
156
+ // is accepted, and the answer for a real domain is a job's business, not a doctor's.
157
+ const { status, json } = await http("https://api.ahrefs.com/v3/public/domain-rating-free?target=example.com", {
142
158
  headers: { Authorization: `Bearer ${env.AHREFS_API_KEY}` },
143
159
  });
144
160
  status === 200
145
- ? ok("AHREFS_API_KEY", `sweepos.app DR ${json?.domain_rating?.domain_rating ?? "?"}`)
161
+ ? ok("AHREFS_API_KEY", `example.com DR ${json?.domain_rating?.domain_rating ?? "?"}`)
146
162
  : fail("AHREFS_API_KEY", `domain-rating-free → ${status}`);
147
163
  }
148
164
 
@@ -164,6 +180,48 @@ async function checkSerper() {
164
180
  return fail("SERPER_API_KEY", json?.message ? `${json.message} (${status})` : `search → ${status}`);
165
181
  }
166
182
 
183
+ async function checkHubspot() {
184
+ const token = env.HUBSPOT_TOKEN;
185
+ if (!token) {
186
+ return skip("HUBSPOT_TOKEN", "unset");
187
+ }
188
+ // limit=1 with no properties: the cheapest call that still proves the token and a scope.
189
+ const { status, json } = await http("https://api.hubapi.com/crm/v3/objects/contacts?limit=1", {
190
+ headers: { Authorization: `Bearer ${token}` },
191
+ });
192
+ if (status === 200) {
193
+ return ok("HUBSPOT_TOKEN", "CRM readable");
194
+ }
195
+ // 403 is a granted-scopes problem, not a bad token — different fix, so say which.
196
+ return fail(
197
+ "HUBSPOT_TOKEN",
198
+ status === 403 ? "token valid but missing CRM read scope (403)" : `contacts → ${status}`,
199
+ );
200
+ }
201
+
202
+ async function checkKleer() {
203
+ const token = env.KLEER_API_TOKEN;
204
+ if (!token) {
205
+ return skip("KLEER_API_TOKEN", "unset");
206
+ }
207
+ // The only POST in the read-only skill: it lists the companies this token can reach, and
208
+ // is the documented way to discover a company id. It changes nothing.
209
+ const { status, json } = await http("https://api.kleer.se/v1/access/authenticate", {
210
+ method: "POST",
211
+ headers: { "X-Token": token, "content-type": "application/json" },
212
+ });
213
+ if (status === 200) {
214
+ const companies = json?.["accessible-companies"];
215
+ const count = Array.isArray(companies)
216
+ ? companies.length
217
+ : Array.isArray(companies?.["accessible-companies"])
218
+ ? companies["accessible-companies"].length
219
+ : "?";
220
+ return ok("KLEER_API_TOKEN", `${count} companies reachable`);
221
+ }
222
+ return fail("KLEER_API_TOKEN", json?.message ? `${json.message} (${status})` : `authenticate → ${status}`);
223
+ }
224
+
167
225
  async function checkCloudflare() {
168
226
  if (!env.CLOUDFLARE_API_KEY || !env.CLOUDFLARE_ACCOUNT_ID) {
169
227
  return skip("CLOUDFLARE", "CLOUDFLARE_API_KEY/ACCOUNT_ID not set");
@@ -466,8 +524,11 @@ async function checkGa4() {
466
524
  }
467
525
 
468
526
  async function checkGoogleAds() {
469
- if (!env.GOOGLE_ADS_DEVELOPER_TOKEN || !env.SWEEPOS_GOOGLE_ADS_CUSTOMER_ID) {
470
- return skip("GOOGLE ADS", "developer token / customer id not set");
527
+ // Any world's customer id proves the developer token and the manager link; which world
528
+ // is this deployment's business, so take the first one it declares.
529
+ const customerId = Object.entries(env).find(([k, v]) => k.endsWith("_GOOGLE_ADS_CUSTOMER_ID") && v)?.[1];
530
+ if (!env.GOOGLE_ADS_DEVELOPER_TOKEN || !customerId) {
531
+ return skip("GOOGLE ADS", "developer token / <WORLD>_GOOGLE_ADS_CUSTOMER_ID not set");
471
532
  }
472
533
  if (!onCloudRun) {
473
534
  return skip("GOOGLE ADS", "needs the metadata server — run on Cloud Run");
@@ -483,7 +544,7 @@ async function checkGoogleAds() {
483
544
  headers["login-customer-id"] = env.GOOGLE_ADS_LOGIN_CUSTOMER_ID;
484
545
  }
485
546
  const { status, json } = await http(
486
- `https://googleads.googleapis.com/v25/customers/${env.SWEEPOS_GOOGLE_ADS_CUSTOMER_ID}/googleAds:searchStream`,
547
+ `https://googleads.googleapis.com/v25/customers/${customerId}/googleAds:searchStream`,
487
548
  { method: "POST", headers, body: JSON.stringify({ query: "SELECT customer.id FROM customer LIMIT 1" }) },
488
549
  );
489
550
  status === 200
@@ -512,9 +573,54 @@ async function checkDeploymentsContext() {
512
573
  : fail("Deployments context (run.viewer)", `list → ${status}`);
513
574
  }
514
575
 
576
+ /**
577
+ * The handful of credentials without which there is no agent at all — as opposed to an
578
+ * integration being unavailable, which degrades one job. A deployment can be missing every
579
+ * other credential here and still work; missing one of these and it is simply down.
580
+ *
581
+ * These are also the ones a shipped script CAN test properly, because they are the
582
+ * engine's own and mean the same thing in every deployment.
583
+ */
584
+ const ESSENTIAL = new Set(["AGENT_API_SECRET", "CLAUDE_CODE_OAUTH_TOKEN", "GITHUB_TOKEN", "GIT_REPO_URL"]);
585
+
586
+ /**
587
+ * Inside a run, three of the four are proven by the fact that this output exists at all —
588
+ * a stronger check than any probe could be. The run is happening, so the Claude credential
589
+ * works; the working directory is the content clone, so GIT_REPO_URL and the token that
590
+ * cloned it work; the request that started it was authenticated, so the API secret works.
591
+ */
592
+ async function checkEssentials() {
593
+ const insideRun = Boolean(env.AGENT_SPAWN_TOKEN);
594
+ if (!env.GIT_REPO_URL) {
595
+ fail("GIT_REPO_URL", "unset — the service has no content repo to clone, and will not boot");
596
+ } else if (insideRun) {
597
+ ok("GIT_REPO_URL", "this run's own clone came from it");
598
+ } else {
599
+ ok("GIT_REPO_URL", "set (not verifiable from here)");
600
+ }
601
+ }
602
+
515
603
  function checkPresence() {
516
- for (const k of ["CLAUDE_CODE_OAUTH_TOKEN", "AGENT_WEBHOOK_SECRET"]) {
517
- env[k] ? ok(k, "present (not remotely testable)") : skip(k, "unset");
604
+ // The token that authorises the whole deployment, and the one credential this check
605
+ // cannot see when it matters most. Run inside a job, `claude` strips its own credential
606
+ // from the environment of every subprocess it spawns — a run cannot read the token that
607
+ // started it, which is exactly right, and means "unset" here would be a lie: the run
608
+ // doing the reporting is itself the proof it is set. AGENT_SPAWN_TOKEN is how we know we
609
+ // are inside a run; the engine sets that one, so `claude` has no reason to strip it.
610
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
611
+ ok("CLAUDE_CODE_OAUTH_TOKEN", "present (not remotely testable)");
612
+ } else if (env.AGENT_SPAWN_TOKEN) {
613
+ ok("CLAUDE_CODE_OAUTH_TOKEN", "in use by this very run — a run cannot read it, by design");
614
+ } else {
615
+ skip("CLAUDE_CODE_OAUTH_TOKEN", "unset");
616
+ }
617
+ // Either name satisfies the engine; say which one is actually in play.
618
+ if (env.AGENT_API_SECRET) {
619
+ ok("AGENT_API_SECRET", "present (not remotely testable)");
620
+ } else if (env.AGENT_WEBHOOK_SECRET) {
621
+ ok("AGENT_API_SECRET", "present under the old name AGENT_WEBHOOK_SECRET");
622
+ } else {
623
+ skip("AGENT_API_SECRET", "unset");
518
624
  }
519
625
  }
520
626
 
@@ -535,11 +641,12 @@ const CHECKS = [
535
641
  ["GITHUB_TOKEN", checkGithub],
536
642
  ["GRAFANA (Loki)", checkGrafana],
537
643
  ["RESEND_API_KEY", checkResend],
538
- ["SWEEPOS internal API", checkInternalApi],
539
- ["SWEEPOS_POSTGRES_URL_READONLY", checkPostgres],
644
+ ["POSTGRES (read-only)", checkPostgres],
540
645
  ["POSTHOG", checkPosthog],
541
646
  ["AHREFS_API_KEY", checkAhrefs],
542
647
  ["SERPER_API_KEY", checkSerper],
648
+ ["HUBSPOT_TOKEN", checkHubspot],
649
+ ["KLEER_API_TOKEN", checkKleer],
543
650
  ["CLOUDFLARE", checkCloudflare],
544
651
  ["ELEVENLABS_API_KEY", checkElevenlabs],
545
652
  ["GOOGLE_API_KEY (Places)", checkPlaces],
@@ -550,6 +657,7 @@ const CHECKS = [
550
657
  ["DWD drive+docs+slides", checkDwdDrive],
551
658
  ["DWD calendar (write scope)", checkDwdCalendar],
552
659
  ["SEARCH CONSOLE", checkSearchConsole],
660
+ ["Essentials", checkEssentials],
553
661
  ["GA4", checkGa4],
554
662
  ["GOOGLE ADS", checkGoogleAds],
555
663
  ["Deployments context (run.viewer)", checkDeploymentsContext],
@@ -561,16 +669,79 @@ await Promise.all(
561
669
  );
562
670
 
563
671
  results.sort((a, b) => a.name.localeCompare(b.name));
564
- const pad = Math.max(...results.map((r) => r.name.length)) + 2;
672
+
673
+ // Configured surfaces get a line each. Unconfigured ones get a single line between them,
674
+ // because this script ships to EVERY deployment and knows every integration the engine
675
+ // supports — which is always more than any one deployment uses. Listing them individually
676
+ // asks the operator "why is Kleer in my report?", and the honest answer is that it is not
677
+ // theirs and never was. The deployment's own register of what it can reach is
678
+ // ENVIRONMENT.md in its content repo; the catalogue of what it could add is `connect`.
679
+ // Credentials this script has never heard of.
680
+ //
681
+ // A deployment adds its own systems, and their keys are just secrets bound to the service:
682
+ // ACME_CRM_TOKEN means nothing to an engine that ships to everyone. Testing one needs its
683
+ // endpoint, its auth header and a notion of what a good answer looks like — none of which
684
+ // can be in here. Silence would be the wrong answer though, because "26 working" reads as
685
+ // "everything works" while a set-but-never-exercised key sits beside it.
686
+ //
687
+ // So: name them, say plainly that nothing here can test them, and let the run that called
688
+ // this reconcile them against ENVIRONMENT.md — the deployment's own register does know
689
+ // what they are and which skill reaches them.
690
+ //
691
+ // Known-ness is decided from this file's own source, so the list maintains itself: if the
692
+ // script mentions the variable, or the family it belongs to, it knows about it.
693
+ // Comments stripped first: a variable named in a comment — including the example two
694
+ // paragraphs up — is not a variable this script can test, and counting it as known is how
695
+ // a real unknown gets silently swallowed.
696
+ const selfSource = readFileSync(fileURLToPath(import.meta.url), "utf8")
697
+ .split("\n")
698
+ .filter((line) => !line.trim().startsWith("//"))
699
+ .join("\n");
700
+ const CREDENTIAL_SHAPED = /_(TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS)$/;
701
+ const unknownCredentials = Object.keys(env)
702
+ .filter((name) => CREDENTIAL_SHAPED.test(name))
703
+ .filter((name) => !selfSource.includes(name) && !selfSource.includes(`${name.split("_")[0]}_`))
704
+ .sort();
705
+
706
+ // An essential can never be "not set up here" — that is the whole point of the tier. Do
707
+ // this BEFORE partitioning, or a promoted failure is counted and never printed.
708
+ for (const missing of results.filter((r) => r.status === "skip" && ESSENTIAL.has(r.name))) {
709
+ missing.status = "FAIL";
710
+ missing.detail = `${missing.detail} — the agent cannot run without it`;
711
+ }
712
+ const configured = results.filter((r) => r.status !== "skip");
713
+ const unconfigured = results.filter((r) => r.status === "skip");
714
+ const pad = Math.max(...configured.map((r) => r.name.length), 1) + 2;
565
715
  console.log(
566
- `\nCredential check — ${onCloudRun ? "Cloud Run (all surfaces)" : "local (metadata-server surfaces skipped)"}\n`,
716
+ `\nCredential check — ${onCloudRun ? "Cloud Run (all surfaces)" : "local (metadata-server surfaces skipped)"}`,
567
717
  );
568
- for (const r of results) {
569
- const icon = r.status === "ok" ? "✅" : r.status === "FAIL" ? "❌" : "⏭️ ";
570
- console.log(`${icon} ${r.name.padEnd(pad)} ${r.status === "ok" ? r.detail : r.detail && `— ${r.detail}`}`);
718
+ const line = (r) => {
719
+ const icon = r.status === "ok" ? "✅" : "❌";
720
+ console.log(` ${icon} ${r.name.padEnd(pad)} ${r.status === "ok" ? r.detail : r.detail && `— ${r.detail}`}`);
721
+ };
722
+ // Split, because a failure in the first group is an outage and a failure in the second is
723
+ // one job doing less. Reading them in one list makes those look like the same news.
724
+ const essential = configured.filter((r) => ESSENTIAL.has(r.name));
725
+ const integrations = configured.filter((r) => !ESSENTIAL.has(r.name));
726
+ if (essential.length) {
727
+ console.log("\nWithout these there is no agent at all");
728
+ essential.forEach(line);
729
+ }
730
+ if (integrations.length) {
731
+ console.log("\nWhat it can reach");
732
+ integrations.forEach(line);
571
733
  }
572
734
  const failures = results.filter((r) => r.status === "FAIL");
573
- console.log(
574
- `\n${results.filter((r) => r.status === "ok").length} ok, ${failures.length} failed, ${results.filter((r) => r.status === "skip").length} skipped`,
575
- );
735
+ console.log(`\n${configured.length - failures.length} working, ${failures.length} failing.`);
736
+ if (unknownCredentials.length) {
737
+ // Names only. This script never prints a value, and that holds hardest for the ones it
738
+ // does not understand.
739
+ console.log(`\nSet, but nothing here knows how to test them (${unknownCredentials.length}):`);
740
+ console.log(` ${unknownCredentials.join(", ")}`);
741
+ console.log(" Each should have a row in ENVIRONMENT.md saying which skill reaches it.");
742
+ }
743
+ if (unconfigured.length) {
744
+ console.log(`\nNot set up here (${unconfigured.length}): ${unconfigured.map((r) => r.name).join(", ")}.`);
745
+ console.log("The engine ships more integrations than any deployment uses — `connect` lists them.");
746
+ }
576
747
  process.exit(failures.length ? 1 : 0);