@meffecta/agent 1.0.8 → 1.0.10
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/engine.json +2 -2
- package/lib/commands.js +27 -0
- package/lib/create-job.js +1 -1
- package/lib/verify-credentials.js +2 -2
- package/package.json +1 -1
- package/scripts/deploy.sh +46 -3
- package/scripts/verify-credentials.mjs +36 -6
package/engine.json
CHANGED
package/lib/commands.js
CHANGED
|
@@ -186,8 +186,35 @@ async function status() {
|
|
|
186
186
|
);
|
|
187
187
|
const external = Boolean(env.AGENT_TASKS_QUEUE && env.AGENT_PUBLIC_URL);
|
|
188
188
|
|
|
189
|
+
// What is RUNNING, asked of the service rather than inferred from the image tag — which
|
|
190
|
+
// is a moving target: `latest` names a different engine every time main is pushed, so the
|
|
191
|
+
// tag on the revision says what was requested and not what answers. /health is
|
|
192
|
+
// unauthenticated, so this needs no credential; a service that cannot be reached simply
|
|
193
|
+
// does not get the line.
|
|
194
|
+
let engine = "";
|
|
195
|
+
if (svc.status?.url) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(`${svc.status.url}/health`, { signal: AbortSignal.timeout(25_000) });
|
|
198
|
+
const health = await res.json();
|
|
199
|
+
if (health?.version) {
|
|
200
|
+
engine = health.build ? `${health.version} (build ${health.build})` : health.version;
|
|
201
|
+
} else {
|
|
202
|
+
engine = "up, but too old to report its version";
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Short timeout on purpose: this is a status command, and asking wakes a
|
|
206
|
+
// scale-to-zero service, so it waits about as long as a cold start and then says
|
|
207
|
+
// nothing rather than hanging. Not a verdict on health — `doctor` is where that is
|
|
208
|
+
// decided.
|
|
209
|
+
engine = "no answer within 25s (a sleeping service can take longer to wake)";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
189
213
|
console.log(`▶ ${d.SERVICE} (${d.PROJECT} / ${d.REGION})`);
|
|
190
214
|
console.log(` url ${svc.status?.url ?? "?"}`);
|
|
215
|
+
if (engine) {
|
|
216
|
+
console.log(` engine ${engine}`);
|
|
217
|
+
}
|
|
191
218
|
console.log(` revision ${svc.status?.latestReadyRevisionName ?? "?"}`);
|
|
192
219
|
console.log(` image ${container.image}`);
|
|
193
220
|
console.log(
|
package/lib/create-job.js
CHANGED
|
@@ -180,7 +180,7 @@ function buildPrompt(description, { timezone, existing }) {
|
|
|
180
180
|
"",
|
|
181
181
|
"Before writing anything, read what this deployment actually is:",
|
|
182
182
|
" - jobs/*.md in your working directory — the house style, and what already exists",
|
|
183
|
-
" - systems/
|
|
183
|
+
" - systems/ — what this deployment can reach, one file per system, and the variables for each",
|
|
184
184
|
" - worlds/ if present — the projects/products a job can be pointed at",
|
|
185
185
|
" - your own available skills — use only skills that exist; never invent a capability",
|
|
186
186
|
"",
|
|
@@ -40,8 +40,8 @@ PART 2 — this deployment's own systems.
|
|
|
40
40
|
|
|
41
41
|
Part 1 tests what the ENGINE knows how to test. It cannot test a system belonging to this
|
|
42
42
|
deployment specifically. The register of those is \`systems/\` in your working directory —
|
|
43
|
-
one file per system
|
|
44
|
-
|
|
43
|
+
one file per system. If it is not there, say so and stop: a deployment with no register has
|
|
44
|
+
no systems of its own that anything can check.
|
|
45
45
|
|
|
46
46
|
Each file's frontmatter says what to do:
|
|
47
47
|
system: what it is
|
package/package.json
CHANGED
package/scripts/deploy.sh
CHANGED
|
@@ -160,6 +160,51 @@ gcloud run deploy "${SERVICE}" \
|
|
|
160
160
|
--execution-environment gen2 \
|
|
161
161
|
"${SCALE_FLAGS[@]}"
|
|
162
162
|
|
|
163
|
+
# What actually landed. `--tag latest` is a request, not an answer: it names whatever GHCR
|
|
164
|
+
# was serving a moment ago, and after this it names whatever it serves next. The digest is
|
|
165
|
+
# the immutable identity — and the one thing that makes a rollback to *this* engine possible
|
|
166
|
+
# in a month, when the tag has moved on.
|
|
167
|
+
read -r URL REVISION < <(gcloud run services describe "${SERVICE}" \
|
|
168
|
+
--project "${PROJECT}" --region "${REGION}" \
|
|
169
|
+
--format 'value[separator=" "](status.url, status.latestReadyRevisionName)' 2>/dev/null || echo " ")
|
|
170
|
+
DIGEST=""
|
|
171
|
+
if [ -n "${REVISION}" ]; then
|
|
172
|
+
DIGEST=$(gcloud run revisions describe "${REVISION}" \
|
|
173
|
+
--project "${PROJECT}" --region "${REGION}" \
|
|
174
|
+
--format 'value(status.imageDigest)' 2>/dev/null || true)
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# And which engine is answering. The digest says the image is different; only the service
|
|
178
|
+
# says what it is. This is also the first real request to the new revision, so a rollout
|
|
179
|
+
# that produced an image which cannot boot says so here rather than at the next cron —
|
|
180
|
+
# generously timed, because a scale-to-zero service is cold and this image is a large pull.
|
|
181
|
+
HEALTH=""
|
|
182
|
+
if [ -n "${URL}" ]; then
|
|
183
|
+
HEALTH=$(curl -fsS --max-time 180 "${URL}/health" 2>/dev/null || true)
|
|
184
|
+
fi
|
|
185
|
+
VERSION=$(printf '%s' "${HEALTH}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
|
186
|
+
BUILD=$(printf '%s' "${HEALTH}" | sed -n 's/.*"build":"\([^"]*\)".*/\1/p')
|
|
187
|
+
|
|
188
|
+
echo ""
|
|
189
|
+
if [ -n "${VERSION}" ]; then
|
|
190
|
+
echo "✅ Running engine ${VERSION}${BUILD:+ (build ${BUILD})}"
|
|
191
|
+
elif [ -n "${HEALTH}" ]; then
|
|
192
|
+
# It answered, but without a version: an engine from before /health reported one.
|
|
193
|
+
echo "✅ Service is up — this engine predates version reporting, so it cannot say which it is"
|
|
194
|
+
else
|
|
195
|
+
echo "⚠️ Deployed, but ${SERVICE} did not answer /health within 3 minutes"
|
|
196
|
+
echo " Check it before assuming the rollout is good:"
|
|
197
|
+
if has_cli; then
|
|
198
|
+
echo " $(cmd logs)"
|
|
199
|
+
else
|
|
200
|
+
echo " gcloud run services logs read ${SERVICE} --project ${PROJECT} --region ${REGION} --limit 50"
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
[ -n "${REVISION}" ] && echo " revision ${REVISION}"
|
|
204
|
+
[ -n "${DIGEST}" ] && echo " image ${DIGEST##*@}"
|
|
205
|
+
[ -n "${DIGEST}" ] && echo " roll back to this exact engine later with:"
|
|
206
|
+
[ -n "${DIGEST}" ] && echo " $(cmd deploy) --image ${DIGEST}"
|
|
207
|
+
|
|
163
208
|
# Only for a deployment that has opted into external triggers — AGENT_TASKS_QUEUE on the
|
|
164
209
|
# service is what says so. Creating Scheduler jobs for an always-on one would mean every
|
|
165
210
|
# cron fires twice, once from outside and once from the in-process timer.
|
|
@@ -170,10 +215,8 @@ if [ "${SYNC_SCHEDULER}" = true ] &&
|
|
|
170
215
|
"$(dirname "${BASH_SOURCE[0]}")/sync-triggers.sh" ${CONFIG_FILE:+--config "${CONFIG_FILE}"}
|
|
171
216
|
fi
|
|
172
217
|
|
|
173
|
-
URL=$(gcloud run services describe "${SERVICE}" \
|
|
174
|
-
--project "${PROJECT}" --region "${REGION}" \
|
|
175
|
-
--format 'value(status.url)' 2>/dev/null || true)
|
|
176
218
|
if [ -n "${URL}" ]; then
|
|
219
|
+
echo ""
|
|
177
220
|
echo "🌍 ${SERVICE} → ${URL}"
|
|
178
221
|
echo " Liveness: curl -fsS ${URL}/health"
|
|
179
222
|
echo " Ask it: ${URL}/ask (any username, AGENT_API_SECRET as the password)"
|
|
@@ -8,7 +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";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
15
|
const env = process.env;
|
|
@@ -674,8 +675,8 @@ results.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
674
675
|
// because this script ships to EVERY deployment and knows every integration the engine
|
|
675
676
|
// supports — which is always more than any one deployment uses. Listing them individually
|
|
676
677
|
// 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
|
-
//
|
|
678
|
+
// theirs and never was. The deployment's own register of what it can reach is `systems/`
|
|
679
|
+
// in its content repo; the catalogue of what it could add is `connect`.
|
|
679
680
|
// Credentials this script has never heard of.
|
|
680
681
|
//
|
|
681
682
|
// A deployment adds its own systems, and their keys are just secrets bound to the service:
|
|
@@ -685,8 +686,8 @@ results.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
685
686
|
// "everything works" while a set-but-never-exercised key sits beside it.
|
|
686
687
|
//
|
|
687
688
|
// So: name them, say plainly that nothing here can test them, and let the run that called
|
|
688
|
-
// this reconcile them against
|
|
689
|
-
//
|
|
689
|
+
// this reconcile them against `systems/` — the deployment's own register does know what
|
|
690
|
+
// they are and which skill reaches them.
|
|
690
691
|
//
|
|
691
692
|
// Known-ness is decided from this file's own source, so the list maintains itself: if the
|
|
692
693
|
// script mentions the variable, or the family it belongs to, it knows about it.
|
|
@@ -736,9 +737,38 @@ console.log(`\n${configured.length - failures.length} working, ${failures.length
|
|
|
736
737
|
if (unknownCredentials.length) {
|
|
737
738
|
// Names only. This script never prints a value, and that holds hardest for the ones it
|
|
738
739
|
// does not understand.
|
|
740
|
+
//
|
|
741
|
+
// Which of them are a PROBLEM, though, is a question this script can answer without
|
|
742
|
+
// knowing anything about the systems themselves: the register travels into every run, so
|
|
743
|
+
// it is right here in the working directory. A credential named in it is deliberate, and
|
|
744
|
+
// proving it works is Part 2's job; one named nowhere is a key no skill will ever reach
|
|
745
|
+
// for, however valid it is. Splitting them matters because a real gap listed among
|
|
746
|
+
// things that are already fine is a gap that gets skimmed past.
|
|
747
|
+
const registered = new Set();
|
|
748
|
+
const registerDir = join(process.cwd(), "systems");
|
|
749
|
+
if (existsSync(registerDir)) {
|
|
750
|
+
for (const entry of readdirSync(registerDir)) {
|
|
751
|
+
if (!entry.endsWith(".md")) continue;
|
|
752
|
+
const text = readFileSync(join(registerDir, entry), "utf8");
|
|
753
|
+
for (const name of unknownCredentials) {
|
|
754
|
+
if (text.includes(name)) registered.add(name);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const unregistered = unknownCredentials.filter((name) => !registered.has(name));
|
|
739
759
|
console.log(`\nSet, but nothing here knows how to test them (${unknownCredentials.length}):`);
|
|
740
760
|
console.log(` ${unknownCredentials.join(", ")}`);
|
|
741
|
-
|
|
761
|
+
if (!existsSync(registerDir)) {
|
|
762
|
+
// No register to read — say the general thing rather than accuse every one of them.
|
|
763
|
+
console.log(" Each needs a file in systems/ saying which skill reaches it.");
|
|
764
|
+
} else {
|
|
765
|
+
if (registered.size) {
|
|
766
|
+
console.log(` In systems/, so a job can reach them: ${[...registered].sort().join(", ")}`);
|
|
767
|
+
}
|
|
768
|
+
if (unregistered.length) {
|
|
769
|
+
console.log(` Not in systems/, so no skill will find them: ${unregistered.join(", ")}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
742
772
|
}
|
|
743
773
|
if (unconfigured.length) {
|
|
744
774
|
console.log(`\nNot set up here (${unconfigured.length}): ${unconfigured.map((r) => r.name).join(", ")}.`);
|