Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Dockerfile.antseed
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*

# Pinned, vendored. Bump deliberately (newer CLIs carry fresher bootstrap nodes).
RUN npm install -g @antseed/cli@0.1.128
# `pg` is the market writer's Postgres client (write-market.js upserts peer_offers
# into the shared host store). NODE_PATH lets the scripts require these globals.
RUN npm install -g @antseed/cli@0.1.128 pg@8.16.3
ENV NODE_PATH=/usr/local/lib/node_modules

COPY antseed/merge-market.js antseed/control.js antseed/entrypoint.sh /usr/local/lib/antseed/
COPY antseed/store.js antseed/write-market.js antseed/write-status.js antseed/control.js antseed/entrypoint.sh /usr/local/lib/antseed/
RUN chmod +x /usr/local/lib/antseed/entrypoint.sh

ENV ANTSEED_DATA_DIR=/data
Expand Down
26 changes: 16 additions & 10 deletions antseed/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
'use strict';
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const { Pool } = require('pg');
const { UPSERT_BUYER_STATUS, buyerStatusRow } = require('./store.js');

const PORT = parseInt(process.env.ANTSEED_CONTROL_PORT || '8379', 10);
const TOKEN = process.env.ANTSEED_CONTROL_TOKEN || '';
const MARKET_DIR = process.env.ANTSEED_MARKET_DIR || '/market';
const STATUS_FILE = MARKET_DIR + '/status-antseed.json';
const PID = process.env.ANTSEED_BUYER_PID || 'antseed';
const DEPOSIT_TIMEOUT_MS = 120000; // on-chain tx
const STATUS_TIMEOUT_MS = 30000;

// One pool for the long-lived control server (write-status.js, the poll-loop
// twin, is one-shot and uses a plain Client instead).
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js ---'
nl -ba antseed/control.js | sed -n '1,240p'

echo
echo '--- searches ---'
rg -n "statement_timeout|query_timeout|connectionTimeoutMillis|Pool\\(|DATABASE_URL|refreshStatus|/deposit|/withdraw|/status" antseed . -g '!node_modules' -g '!dist' -g '!build'

Repository: genlayerlabs/unhardcoded

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package metadata to identify pg version and any timeout-related docs/config.
echo '--- package files mentioning pg ---'
rg -n '"pg"|node-postgres|postgres' package.json antseed package.json . -g '!node_modules' -g '!dist' -g '!build'

echo
echo '--- antseed/control.js nearby context (if file is large) ---'
wc -l antseed/control.js

Repository: genlayerlabs/unhardcoded

Length of output: 1648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js ---'
nl -ba antseed/control.js | sed -n '1,240p'

echo
echo '--- timeout-related search ---'
rg -n "statement_timeout|query_timeout|connectionTimeoutMillis|Pool\\(|DATABASE_URL|refreshStatus|/deposit|/withdraw|/status" . -g '!node_modules' -g '!dist' -g '!build'

echo
echo '--- package metadata ---'
if [ -f package.json ]; then
  sed -n '1,220p' package.json
fi
if [ -f antseed/package.json ]; then
  sed -n '1,220p' antseed/package.json
fi

Repository: genlayerlabs/unhardcoded

Length of output: 229


🌐 Web query:

node-postgres Pool connectionTimeoutMillis query_timeout statement_timeout documentation

💡 Result:

In node-postgres, the Pool configuration supports several timeout parameters that govern different stages of the database interaction lifecycle [1][2]. connectionTimeoutMillis This parameter controls the maximum time, in milliseconds, that the client will wait to establish a connection to the database [3][4]. If a connection cannot be acquired from the pool within this time, it returns an error [3][4]. The default is no timeout [1][2]. query_timeout This is a client-side timeout managed by the node-postgres library [4][5]. It specifies the maximum time, in milliseconds, that the library will wait for a response from the database after a query has been sent [1][4][5]. If the time is exceeded, the driver stops listening for a response, but the database may continue executing the query on the server side [5]. The default is no timeout [1][2]. statement_timeout This parameter is passed directly to the PostgreSQL server [5]. It defines the maximum time, in milliseconds, that an individual SQL statement is allowed to run on the server [1][4]. If the limit is reached, PostgreSQL cancels the statement and returns an error to the client [4][5]. This is generally considered the more robust way to handle long-running queries, as it ensures the server stops processing the request [5]. The default is no timeout [1][2]. Recommended Relationship Best practice often involves using both, setting the query_timeout slightly longer than the statement_timeout [6][5]. This allows the database to gracefully cancel and report the error (via statement_timeout), while providing a client-side safety net (via query_timeout) in the event of network issues or other failures where the database fails to send a response [5]. Configuration These options can be passed directly to the Pool constructor [3][7]: const pool = new Pool({ connectionTimeoutMillis: 3000, statement_timeout: 20000, query_timeout: 25000 }); [8], [3], [1], [4], [7], [2], [5], [9]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js (first 260 lines with line numbers via awk) ---'
awk '{printf "%4d  %s\n", NR, $0}' antseed/control.js | sed -n '1,260p'

echo
echo '--- pool/timeout usage in antseed ---'
grep -RInE 'Pool\\(|connectionTimeoutMillis|query_timeout|statement_timeout|refreshStatus|/deposit|/withdraw|/status' antseed || true

echo
echo '--- antseed/control.js line count ---'
wc -l antseed/control.js

Repository: genlayerlabs/unhardcoded

Length of output: 6055


Add a timeout to the shared Postgres pool. refreshStatus() awaits pool.query(), so a wedged database can leave successful /deposit, /withdraw, and /status responses hanging indefinitely. antseed/control.js:26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/control.js` at line 26, The shared Postgres pool created in
control.js currently has no timeout, so pool.query() calls in refreshStatus()
can hang indefinitely when the database is wedged. Update the Pool
initialization for the shared pool to include an appropriate timeout setting,
and make sure the existing refreshStatus(), /deposit, /withdraw, and /status
flows use that same pool instance so requests fail fast instead of waiting
forever.


if (!TOKEN) {
console.error('[control] ANTSEED_CONTROL_TOKEN unset — control server disabled');
return;
Expand All @@ -42,20 +46,22 @@ function run(args, timeout) {
});
}

// Refresh /market/status-antseed.json from a fresh `buyer status --json`, in the
// same shape the entrypoint writer uses (fetched_at_ms + atomic rename) so the
// router's source picks up the new escrow balance on its next read.
// Upsert the buyer's status into the host store (buyer_status) from a fresh
// `buyer status --json`, so the router's source picks up the new escrow balance
// on its next read — the post-wallet-op twin of write-status.js. Returns the
// fresh status object for the HTTP response even if the persist fails (the poll
// loop will retry the write); null only when the CLI output isn't a status.
async function refreshStatus() {
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
const tmp = STATUS_FILE + '.' + process.pid + '.tmp';
try {
fs.writeFileSync(tmp, JSON.stringify(data));
fs.renameSync(tmp, STATUS_FILE);
} catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} }
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));
Comment on lines 55 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not trust non-zero status CLI output.

run() returns code, but refreshStatus() ignores it. If the CLI exits non-zero while emitting JSON, this path can still return ok: true and upsert bad status data.

Suggested guard
   const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
+  if (r.code !== 0) return null;
   let data;
   try { data = JSON.parse(r.stdout); } catch (_) { return null; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
const tmp = STATUS_FILE + '.' + process.pid + '.tmp';
try {
fs.writeFileSync(tmp, JSON.stringify(data));
fs.renameSync(tmp, STATUS_FILE);
} catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} }
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
if (r.code !== 0) return null;
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
try {
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/control.js` around lines 55 - 61, The refreshStatus() flow currently
parses stdout from run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS) without
checking the returned exit code, so non-zero CLI failures can still be treated
as valid status data. Update refreshStatus() to inspect the run() result code
before JSON parsing/upsert, and immediately return a failed/null status when the
CLI exits non-zero; keep the existing JSON parse and
pool.query/UPSERT_BUYER_STATUS path only for successful runs.

} catch (e) {
console.error('[control] buyer_status upsert failed:', e.message);
}
return data;
}

Expand Down
47 changes: 24 additions & 23 deletions antseed/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,44 @@
# socat exposes it on the container network. This lets us DROP
# `network_mode: service:router`, whose orphaned netns (router recreated,
# antseed not) silently zeroed discovery on every deploy.
# * validate-before-write -> never clobber market.json with the CLI's
# non-JSON "No peers found" line.
# * validate-before-write -> the writers reject the CLI's non-JSON
# "No peers found" line (exit non-zero) instead of wiping the store row.
# * --top high -> the CLI default is 20; we want the whole
# peer set.
#
# The market book and buyer status are written to the host store (Postgres)
# by write-market.js / write-status.js — no market.json / status-*.json files.
# $MARKET_DIR is now just container-local scratch for the raw CLI dumps.
set -eu

PORT_PROXY="${ANTSEED_PROXY_PORT:-8377}" # buyer proxy (binds 127.0.0.1)
PORT_PUBLIC="${ANTSEED_PUBLIC_PORT:-8378}" # socat, on the container network
MARKET_DIR="${ANTSEED_MARKET_DIR:-/market}"
TOP="${ANTSEED_BROWSE_TOP:-500}"
INTERVAL="${ANTSEED_BROWSE_INTERVAL:-60}" # browse cadence (s); 60s feeds the
# sliding window in merge-market.js
# observed_at sliding window in peer_offers
MAXIN="${ANTSEED_MAX_INPUT:-1000}" # buyer spend rail; Σ_pol policy is
MAXOUT="${ANTSEED_MAX_OUTPUT:-1000}" # the real per-call price ceiling
CLI_TIMEOUT="${ANTSEED_CLI_TIMEOUT:-45}" # hard cap per browse/status CLI call.
# WITHOUT it a single hung `antseed`
# invocation (e.g. a concurrent buyer
# command grabbing the sqlite store)
# blocks the writer loop FOREVER — it
# has no self-restart — so market.json
# and status-antseed.json silently
# freeze and the catalog/wallet go stale.
# has no self-restart — so the
# peer_offers / buyer_status rows
# silently freeze and the catalog/wallet go stale.
LIB=/usr/local/lib/antseed

mkdir -p "$MARKET_DIR"

# A CHANGE_ME / non-hex ANTSEED_IDENTITY_HEX (the unset-secret placeholder) would
# make the CLI reject the identity; unset it so the buyer falls back to a
# generated key on the data volume — durable enough for dev, and the prod secret
# is a real 64-hex hot-wallet.
if ! printf '%s' "${ANTSEED_IDENTITY_HEX:-}" | grep -qiE '^[0-9a-f]{64}$'; then
unset ANTSEED_IDENTITY_HEX
fi

# Buyer proxy in browse mode (no --peer: the host pins per request). A funded
# wallet is needed to transact; discovery/pricing work unfunded.
antseed buyer start -p "$PORT_PROXY" \
Expand All @@ -50,24 +62,12 @@ socat "TCP-LISTEN:${PORT_PUBLIC},fork,reuseaddr" "TCP:127.0.0.1:${PORT_PROXY}" &
# Self-disables when ANTSEED_CONTROL_TOKEN is unset. See antseed/control.js.
node "$LIB/control.js" &

atomic_write() { # <validator-cmd> <dest> ; reads producer stdout on fd via $1
dest="$1"; tmp="${dest}.$$.tmp"
if cat > "$tmp" && [ -s "$tmp" ]; then
mv "$tmp" "$dest"
else
rm -f "$tmp"
return 1
fi
}

write_market() {
raw="$MARKET_DIR/.market.raw.$$"
timeout -k 5 "$CLI_TIMEOUT" antseed network browse --services --top "$TOP" --json > "$raw" 2>/dev/null || true
# merge this browse into the rolling window (and validate: a non-dump exits
# non-zero -> keep the last good market.json)
if node "$LIB/merge-market.js" "$MARKET_DIR/market.json" < "$raw" | atomic_write "$MARKET_DIR/market.json"; then
:
else
# upsert this browse into the host store's peer_offers (and validate: a
# non-dump / DB error exits non-zero -> keep the last good window, no write)
if ! node "$LIB/write-market.js" < "$raw"; then
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# record what we got for debugging; keep the last good window
cp "$raw" "$MARKET_DIR/market.err" 2>/dev/null || true
fi
Expand All @@ -77,8 +77,9 @@ write_market() {
write_status() {
raw="$MARKET_DIR/.status.raw.$$"
timeout -k 5 "$CLI_TIMEOUT" antseed buyer status --json > "$raw" 2>/dev/null || true
node -e 'const fs=require("fs");let d;try{d=JSON.parse(fs.readFileSync(0,"utf8"))}catch(e){process.exit(2)}if(d===null||typeof d!=="object")process.exit(3);d.fetched_at_ms=Date.now();process.stdout.write(JSON.stringify(d))' \
< "$raw" | atomic_write "$MARKET_DIR/status-antseed.json" || true
# upsert the buyer status into the host store's buyer_status (validate + DB
# error -> exit non-zero, keep the last good row)
node "$LIB/write-status.js" < "$raw" || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
git ls-files 'antseed/*' | sed -n '1,200p'

# Map the entrypoint and status writer structure
ast-grep outline antseed/entrypoint.sh --view expanded || true
ast-grep outline antseed/write-status.js --view expanded || true
ast-grep outline antseed/lib/write-status.js --view expanded || true

# Read the relevant script sections with line numbers
for f in antseed/entrypoint.sh antseed/write-status.js antseed/lib/write-status.js; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '1,220p'
  fi
done

# Find PostgreSQL usage and any timeout handling in the antseed directory
rg -n --hidden --no-messages 'timeout|pg\.|new Client|Client\(|pool|connect\(|query\(' antseed

Repository: genlayerlabs/unhardcoded

Length of output: 8136


Bound the status writer’s DB call. node "$LIB/write-status.js" can still block in pg connect/query because write-status.js has no timeout, so || true only masks exited failures. Wrap this invocation in timeout or add a DB timeout in antseed/write-status.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/entrypoint.sh` at line 74, The status writer call in the entrypoint
can still hang during pg connect/query because the current fallback only ignores
exit failures. Update the `node "$LIB/write-status.js"` invocation in
`entrypoint.sh` to run under a timeout, or add an explicit database timeout
inside `write-status.js` so the `writeStatus` flow cannot block indefinitely.

rm -f "$raw"
}

Expand Down
57 changes: 0 additions & 57 deletions antseed/merge-market.js

This file was deleted.

27 changes: 27 additions & 0 deletions antseed/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Shared host-store writes for the antseed sidecar (write-status.js on the poll
// loop and control.js after a wallet op both upsert the buyer status). One place
// for the buyer_status row shape + UPSERT so the two writers can't drift.
//
// Raw buyer-reported fields as columns; the deposits are kept as the strings the
// buyer reports (sources/antseed coerces on read). No interpretation here.
const UPSERT_BUYER_STATUS = `INSERT INTO buyer_status
(pid, pinned_peer_id, deposits_available, deposits_reserved, wallet_address,
connection_state, fetched_at)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT (pid) DO UPDATE SET
pinned_peer_id=EXCLUDED.pinned_peer_id,
deposits_available=EXCLUDED.deposits_available,
deposits_reserved=EXCLUDED.deposits_reserved,
wallet_address=EXCLUDED.wallet_address,
connection_state=EXCLUDED.connection_state,
fetched_at=EXCLUDED.fetched_at`;

const str = (v) => (v === null || v === undefined) ? null : String(v);

function buyerStatusRow(d, pid) {
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];
Comment on lines +21 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject non-status objects before shaping the UPSERT row.

buyerStatusRow({}) currently produces a row of null status fields, so object-shaped CLI errors can clobber the last good buyer_status row. Centralize a shape check here and have callers use it before connecting/upserting.

Suggested shared validator
 const str = (v) => (v === null || v === undefined) ? null : String(v);
+const STATUS_KEYS = [
+  "pinnedPeerId", "depositsAvailable", "depositsReserved",
+  "walletAddress", "connectionState",
+];
+
+function isBuyerStatus(d) {
+  return d !== null && typeof d === "object" &&
+    STATUS_KEYS.some((k) => Object.prototype.hasOwnProperty.call(d, k));
+}
 
 function buyerStatusRow(d, pid) {
+  if (!isBuyerStatus(d)) throw new TypeError("not an antseed buyer status");
   return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
           str(d.depositsReserved), str(d.walletAddress),
           str(d.connectionState), Date.now()];
 }
 
-module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow };
+module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function buyerStatusRow(d, pid) {
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];
const str = (v) => (v === null || v === undefined) ? null : String(v);
const STATUS_KEYS = [
"pinnedPeerId", "depositsAvailable", "depositsReserved",
"walletAddress", "connectionState",
];
function isBuyerStatus(d) {
return d !== null && typeof d === "object" &&
STATUS_KEYS.some((k) => Object.prototype.hasOwnProperty.call(d, k));
}
function buyerStatusRow(d, pid) {
if (!isBuyerStatus(d)) throw new TypeError("not an antseed buyer status");
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];
}
module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/store.js` around lines 21 - 24, The UPSERT row builder in
buyerStatusRow currently accepts any object and turns missing fields into nulls,
which can overwrite a valid buyer_status record. Add a shared shape validator
for status objects and use it before calling buyerStatusRow and before any
connect/upsert path so only properly shaped status data is accepted. Keep the
check centralized near buyerStatusRow and update callers to reject invalid CLI
error objects before row creation.

}

module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow };
85 changes: 85 additions & 0 deletions antseed/write-market.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Marketplace book writer (replaces merge-market.js). Each browse sees only the
// peers DHT-reachable from this node *right now* (~68); the network has more
// (~83) that flit in and out of our view. We keep a SLIDING window so a peer
// silent for a few browses still routes: instead of a hand-rolled union of the
// previous file, every peer/service we see is UPSERT-ed into the host store's
// `peer_offers` table stamped `observed_at = now`, and sources/antseed.py reads
// only rows within the window (WHERE observed_at >= now - window). The window
// thus lives as a read-time filter; here we just record what we saw and prune
// rows older than the window so the table can't grow without bound.
//
// One RAW row per (peerId, service): the seller's announced prices/cap/reputation
// as columns, no interpretation — ranking/admission stays in offers_sync (host)
// and the Σ_pol policy (core). Type-cleaning (positive-int cap, numeric
// reputation, non-null cached price) happens here, at the write, mirroring the
// coercion sources/antseed.py used to do at the read.
//
// Validation: a non-dump (the CLI prints a human "No peers found" line even with
// --json) exits non-zero so entrypoint.sh keeps the last good window (no write).
const { Client } = require("pg");

const WINDOW_MS = Number(process.env.ANTSEED_PEER_WINDOW_MS || 15 * 60 * 1000);
const now = Date.now();

function numOr0(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; }
function numOrNull(v) {
if (v === null || v === undefined) return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
function posIntOrNull(v) { // maxConcurrency: positive int, else ungated (null)
const n = Number(v);
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
}

let fresh;
try { fresh = JSON.parse(require("fs").readFileSync(0, "utf8")); }
catch (e) { process.exit(2); } // not JSON
if (fresh === null || typeof fresh !== "object" || !Array.isArray(fresh.peers))
process.exit(3); // JSON but not a dump

// Flatten the nested browse dump (peer -> providerPricing -> services) to one
// row per (peerId, service). No services -> the peer contributes no rows.
const rows = [];
for (const peer of fresh.peers) {
if (!peer || !peer.peerId) continue;
const maxc = posIntOrNull(peer.maxConcurrency);
const rep = numOrNull(peer.onChainReputationScore);
const lastSeen = numOrNull(peer.lastSeen);
for (const pricing of Object.values(peer.providerPricing || {})) {
for (const [service, sp] of Object.entries((pricing || {}).services || {})) {
rows.push([
peer.peerId, service,
numOr0(sp.inputUsdPerMillion), numOr0(sp.outputUsdPerMillion),
numOrNull(sp.cachedInputUsdPerMillion),
maxc, rep, lastSeen, now, now, now,
]);
}
}
}

const UPSERT = `INSERT INTO peer_offers
(peer_id, service, price_in, price_out, price_cached_in, max_concurrency,
reputation, last_seen, observed_at, first_seen, fetched_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
ON CONFLICT (peer_id, service) DO UPDATE SET
price_in=EXCLUDED.price_in, price_out=EXCLUDED.price_out,
price_cached_in=EXCLUDED.price_cached_in,
max_concurrency=EXCLUDED.max_concurrency, reputation=EXCLUDED.reputation,
last_seen=EXCLUDED.last_seen, observed_at=EXCLUDED.observed_at,
fetched_at=EXCLUDED.fetched_at`; // first_seen preserved across conflicts

(async () => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
try {
await client.connect();
for (const r of rows) await client.query(UPSERT, r);
await client.query("DELETE FROM peer_offers WHERE observed_at < $1",
[now - WINDOW_MS]);
} catch (e) {
process.stderr.write(`write-market: ${e.message}\n`);
process.exitCode = 4; // DB error -> keep window
} finally {
await client.end().catch(() => {});
Comment on lines +72 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== antseed/write-market.js ==\n'
wc -l antseed/write-market.js
sed -n '1,140p' antseed/write-market.js

printf '\n== antseed/entrypoint.sh ==\n'
wc -l antseed/entrypoint.sh
sed -n '1,220p' antseed/entrypoint.sh

printf '\n== transaction usage search ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '\b(BEGIN|COMMIT|ROLLBACK)\b|client\.query\("BEGIN"|client\.query\("COMMIT"|client\.query\("ROLLBACK"' antseed . || true

Repository: genlayerlabs/unhardcoded

Length of output: 8600


Wrap the refresh in a transaction.
write-market.js autocommits each UPSERT and the prune separately. If a later row or the DELETE fails, earlier rows stay committed even though entrypoint.sh treats the non-zero exit as “keep the last good window,” leaving a partially refreshed peer_offers.

🧰 Tools
🪛 ast-grep (0.44.0)

[error] 76-77: Avoid SQL injection
Context: client.query("DELETE FROM peer_offers WHERE observed_at < $1",
[now - WINDOW_MS])
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-javascript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/write-market.js` around lines 72 - 83, The refresh logic in
write-market.js is committing each UPSERT and the prune independently, so a
failure can leave peer_offers partially updated. Update the main async flow that
uses Client.connect(), the rows loop, and the DELETE from peer_offers to run
inside a single database transaction, and make sure any error triggers a
rollback before the existing stderr/exitCode handling.

}
})();
29 changes: 29 additions & 0 deletions antseed/write-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Buyer status writer (replaces the inline `node -e` + status-antseed.json).
// Reads `antseed buyer status --json` from stdin and UPSERTs the buyer's status
// (session pin + escrow + wallet) into the host store's buyer_status, keyed by
// the buyer pid. sources/antseed.py reads it (_pinned_peer + balances).
//
// Validation: a non-object / non-JSON exits non-zero so entrypoint.sh keeps the
// last good row (no write).
const { Client } = require("pg");
const { UPSERT_BUYER_STATUS, buyerStatusRow } = require("./store.js");

const PID = process.env.ANTSEED_BUYER_PID || "antseed";

let d;
try { d = JSON.parse(require("fs").readFileSync(0, "utf8")); }
catch (e) { process.exit(2); } // not JSON
if (d === null || typeof d !== "object") process.exit(3); // JSON but not a status

(async () => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
try {
await client.connect();
await client.query(UPSERT_BUYER_STATUS, buyerStatusRow(d, PID));
Comment on lines +16 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the status shape before opening Postgres.

With the current object-only check, {} or {"error": ...} reaches the UPSERT path. After adding the shared validator in store.js, use it here so invalid status exits before any DB work.

Suggested caller update
-const { UPSERT_BUYER_STATUS, buyerStatusRow } = require("./store.js");
+const { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus } = require("./store.js");
 ...
-if (d === null || typeof d !== "object") process.exit(3); // JSON but not a status
+if (!isBuyerStatus(d)) process.exit(3);                   // JSON but not a status
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/write-status.js` around lines 16 - 22, The current object-only guard
in write-status.js lets invalid status objects like {} or {"error": ...} proceed
into the UPSERT path and open Postgres unnecessarily. Reuse the shared status
validator from store.js in the write-status entrypoint before creating the
Client, and exit early for any invalid shape so only a valid status reaches
buyerStatusRow and UPSERT_BUYER_STATUS.

} catch (e) {
process.stderr.write(`write-status: ${e.message}\n`);
process.exitCode = 4; // DB error -> keep row
} finally {
await client.end().catch(() => {});
}
})();
Loading