472 lines
23 KiB
Go
472 lines
23 KiB
Go
package main
|
|
|
|
// contextprep.go — what this machine can turn into TEXT.
|
|
//
|
|
// OCR and the other common data-preparation tools belong here because
|
|
// preparing context is the bastion's whole job. A video decoder, likewise, is
|
|
// a user thing — never a platform service — so it has to be something the user
|
|
// runs.
|
|
//
|
|
// The principle that makes those one family: ANYTHING THAT TURNS A USER'S
|
|
// BYTES INTO THEIR CONTENT RUNS ON THEIR HARDWARE. Same reason embeddings went
|
|
// to the bastion, and stronger — an embedding is a vector, but OCR output is
|
|
// the literal text of their document and a transcript is the literal words
|
|
// they said.
|
|
//
|
|
// This file is the ADVERTISEMENT half only: which of those tools this box
|
|
// actually has. It never runs one on a document; that is the ctx_extract op.
|
|
//
|
|
// ── why one capability with a list, and not one token per tool ──────────────
|
|
//
|
|
// The host's question is always "can this box turn THIS mime into text", which
|
|
// is a lookup, not six separate checks. The heartbeat already carries lists
|
|
// beside capabilities for exactly this (models, llm_models, schedulers —
|
|
// agents.go:448-479), so the shape exists; six tokens would be six flag days
|
|
// for no gain.
|
|
//
|
|
// ── why the probe RUNS the tool ─────────────────────────────────────────────
|
|
//
|
|
// detectRuntime (harness.go:580) runs `docker ps -q` and takes the exit code
|
|
// rather than looking for a file, because a binary on PATH that cannot execute
|
|
// is not a capability. Same rule here.
|
|
//
|
|
// MEASURED, and it is why the version is read from BOTH streams:
|
|
// `pdftotext -v` writes its banner to STDERR and exits 0, while `tesseract
|
|
// --version` and `ffmpeg -version` write to STDOUT. runCmd returns clean
|
|
// stdout on success, so a version read through it reports poppler as blank
|
|
// while poppler works perfectly. The CAPABILITY decision is the exit code
|
|
// alone; the version string is for the operator's eyes and must not be allowed
|
|
// to influence it.
|
|
//
|
|
// ── quiet vs blind ─────────────────────────────────────────────────────────
|
|
//
|
|
// A box that has no tools and a box that could not look must not read the same
|
|
// to the host. That distinction is already made three times in this heartbeat
|
|
// (agent-observe, computer-observe, scheduler-observe — agents.go:423-444), so
|
|
// the scan report travels the way schedScanReport does.
|
|
|
|
import (
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// prepCapability is the single token. Additive: agents.go:359-360 records that
|
|
// hosts predating a capability ignore one they do not know, so this needs no
|
|
// host-side flag day and an older host simply never offers the feature.
|
|
const prepCapability = "context-prep"
|
|
|
|
// prepGateOp is the op name this capability is gated by, and the name is
|
|
// LOAD-BEARING. capsAllow maps an op to a family through opFamily
|
|
// (anchor.go:411-427), and opFamily returns "" for a name it does not
|
|
// recognise — whereupon capsAllow ALLOWS it (anchor.go:476-479). So an op
|
|
// called "prep_extract" would be UNGATED on an anchored box, silently. The
|
|
// "ctx_" prefix puts it in the dcn family, beside ctx_* and backup_fetch,
|
|
// which is where it belongs on the merits too: like backup_fetch it pulls the
|
|
// host's bytes onto the user's machine. Nothing in anchor.go needs to change
|
|
// and no seventh caps family has to be invented.
|
|
//
|
|
// prepGateOpFamily is asserted against opFamily in the test, so renaming the
|
|
// op without thinking about the family fails a gate instead of quietly opening
|
|
// this lane to every member pairing.
|
|
const prepGateOp = "ctx_extract"
|
|
const prepGateOpFamily = "dcn"
|
|
|
|
// prepCacheFor matches scanSchedulers' cadence (schedulers.go:435) for the
|
|
// same reason given there: the heartbeat is every 60 s, this is a handful of
|
|
// shell-outs, and installing a tool should show up within minutes without
|
|
// anyone restarting the daemon.
|
|
const prepCacheFor = 10 * time.Minute
|
|
|
|
// prepTool is one thing this box might be able to do, the binary that would do
|
|
// it, and an argv that proves the binary RUNS.
|
|
//
|
|
// EVERY probe argv below was executed against the real tool and
|
|
// its exit code recorded — none is copied from documentation. A tool whose
|
|
// interface has not been verified does not belong in this table: advertising
|
|
// it would be the exact dishonesty the quiet-vs-blind rule exists to prevent.
|
|
type prepTool struct {
|
|
// Name is what the HOST asks for. It names the job, not the program, so
|
|
// swapping poppler for something else later is not a host-visible change.
|
|
Name string
|
|
Bin string
|
|
Args []string
|
|
// Builtin marks a job THIS BINARY does, with no program on the machine to
|
|
// probe: office-text reads docx/xlsx/pptx out of Go's own archive/zip and
|
|
// encoding/xml, so there is nothing to install and nothing to exec.
|
|
//
|
|
// It does not weaken the honesty rule, it moves where the rule bites. A
|
|
// builtin still has to PROVE itself below, and office-text's Prove runs the
|
|
// whole reader over a package assembled in memory. What a builtin removes is
|
|
// the "is it installed" question, which for a builtin has exactly one answer.
|
|
Builtin bool
|
|
// Needs are the OTHER binaries this job cannot run without, each with an
|
|
// argv that proves IT runs. pdf-ocr rasterizes with poppler before it hands
|
|
// a page to the engine, and a box holding one of the two must not advertise
|
|
// the job. Probed exactly like Bin, after it.
|
|
Needs []prepTool
|
|
// Prove, when set, is the LAST word on availability and overrides nothing
|
|
// before it — every binary must still answer first. It exists because an
|
|
// exit code is sometimes a PROXY for the capability rather than the
|
|
// capability: `tesseract --version` and `tesseract --list-langs` both exit 0
|
|
// on a machine with no language data installed, which cannot read a word
|
|
// (measured, contextocr.go's header). Returning a REASON is the point: "the
|
|
// engine is here and has no language data" and "there is no engine" are
|
|
// different answers that the user fixes with different commands.
|
|
Prove func() (bool, string)
|
|
// Langs, when set, is what this job can read. It travels beside the tool so
|
|
// a user can see which language pack their machine is using without asking
|
|
// it anything.
|
|
Langs func() []string
|
|
// Models, when set, is the MODEL FILES this job can load, best-first.
|
|
//
|
|
// Unlike Langs this is not merely informational, and transcription is why it
|
|
// exists. "I have poppler" is the whole answer for a PDF; "I have a speech
|
|
// engine" is not the answer for a recording, because base.en and
|
|
// large-v3-turbo are three times apart in error rate on hard audio and eleven
|
|
// times apart in CPU (measured, contextstt.go). A host choosing a machine for
|
|
// a recording is choosing a model whether it knows it or not, so the list
|
|
// travels and the host may name one.
|
|
Models func() []string
|
|
}
|
|
|
|
// prepToolTable is every job this build knows how to probe for.
|
|
//
|
|
// DELIBERATELY ABSENT, and each for a stated reason rather than an oversight:
|
|
// - transcribe: the implementation is not chosen. whisper.cpp (a binary),
|
|
// openai-whisper (python) and faster-whisper (a library) differ in what the
|
|
// user must install, not merely in speed, so the pick is partly a product
|
|
// call. The rule is to benchmark rather than pick from reputation; until that
|
|
// runs there is no argv to verify and nothing honest to advertise.
|
|
//
|
|
// Adding it means verifying its argv the same way, not appending a row.
|
|
var prepToolTable = []prepTool{
|
|
// Text PDFs — the largest gap by volume, and no OCR needed for them.
|
|
// -v exits 0 and prints to stderr (measured).
|
|
{Name: "pdf-text", Bin: "pdftotext", Args: []string{"-v"}},
|
|
// OCR of a picture: the literal words on it, read by the engine.
|
|
//
|
|
// Args is the cheap "does the binary run" gate; Prove is what actually
|
|
// decides, because --version exits 0 on a machine with no language data.
|
|
// contextocr.go's header has the measurement.
|
|
{Name: "ocr", Bin: "tesseract", Args: []string{"--version"},
|
|
Prove: ocrProve, Langs: ocrLangs},
|
|
// A scanned PDF: its own text layer where it has one, the engine where it
|
|
// does not. Needs BOTH poppler tools as well as the engine — the layer pass
|
|
// reads the text and carries the page count, the rasterizer draws the pages
|
|
// that need the engine — which is why this is a separate job from "ocr" and
|
|
// not the same one pointed at a PDF.
|
|
//
|
|
// Bin is the engine rather than poppler on purpose: a box with no tesseract
|
|
// reports this job as simply absent (it also has no "ocr"), while a box WITH
|
|
// the engine and no pdftoppm reports it as blocked and names the missing
|
|
// piece. The first binary probed is the one whose absence is uninteresting.
|
|
{Name: "pdf-ocr", Bin: "tesseract", Args: []string{"--version"},
|
|
Needs: []prepTool{
|
|
{Name: "rasterize", Bin: "pdftoppm", Args: []string{"-v"}},
|
|
{Name: "pdf-layer", Bin: "pdftotext", Args: []string{"-v"}},
|
|
},
|
|
Prove: ocrProve, Langs: ocrLangs},
|
|
// Office documents: docx, xlsx and pptx, read by THIS BINARY.
|
|
//
|
|
// soffice was unverified, so it was installed and run
|
|
// before this row was written. It is not used, and contextoffice.go's header
|
|
// carries the four measurements that decided it — the one that matters most
|
|
// here is that two concurrent headless conversions against one user profile
|
|
// lose one of the pair in 5 of 5 trials, silently, with nothing on either
|
|
// stream. This box runs mgmtWorkerCount management workers.
|
|
//
|
|
// No Bin and no Args because there is no program: these three formats are
|
|
// ZIPs of XML and Go's standard library reads both halves. THE USER INSTALLS
|
|
// NOTHING, which on a lane that runs on their own machine is the criterion
|
|
// that decides between two tools that would both work.
|
|
{Name: "office-text", Builtin: true, Prove: officeProve},
|
|
// A clip's FRAMES (contextframes.go). Two binaries, because the job is two
|
|
// steps: ffprobe reads the container's keyframe index without decoding
|
|
// anything, and ffmpeg then decodes only the frames the plan asks for. A box
|
|
// with one and not the other cannot finish the job, so Needs carries the
|
|
// second — the same structure pdf-ocr uses for its rasterizer.
|
|
//
|
|
// Both argvs were run against the real binaries (ffmpeg 8.0.1):
|
|
// each exits 0 and banners to stdout.
|
|
//
|
|
// This job's answer is PICTURES, not text, so its runner is frameTools
|
|
// rather than extractArgv or extractMulti — see contextframes.go for why the
|
|
// reply has a second shape instead of a text field holding base64.
|
|
{Name: ctxFramesTool, Bin: "ffmpeg", Args: []string{"-version"},
|
|
Needs: []prepTool{
|
|
{Name: "probe", Bin: "ffprobe", Args: []string{"-version"}},
|
|
}},
|
|
// SPEECH TO TEXT — the words the user actually said (contextstt.go).
|
|
//
|
|
// Two rows for one engine, and the split is the same one pdf-text/pdf-ocr
|
|
// makes: whisper-cli reads wav, mp3, flac and ogg BY ITSELF and FAILS on mp4
|
|
// and m4a (measured against the real binary; its own help prints the list).
|
|
// So a box with the engine alone can transcribe every voice memo the user
|
|
// owns, and a box with ffmpeg as well can also transcribe a clip's audio
|
|
// track. One row would either refuse the first box or lie to the second.
|
|
//
|
|
// `--version` exits 0 in 0.01s and banners to stdout (measured). It is the
|
|
// cheap gate only: Prove is what decides, because an installed engine WITH NO
|
|
// MODEL also exits 0 — and that is the normal state after installing it, the
|
|
// Ubuntu 26.04 package shipping no model file at all. Prove loads a model and
|
|
// transcribes a known input, and it TIMES that run, which is where this job's
|
|
// declared ceiling comes from.
|
|
{Name: sttTool, Bin: sttBin, Args: []string{"--version"},
|
|
Prove: sttProve, Models: sttModelsReport},
|
|
// Bin is the ENGINE rather than ffmpeg on purpose, the same way pdf-ocr's is
|
|
// tesseract rather than poppler: a box with no whisper-cli reports this job
|
|
// as simply absent (it has no "transcribe" either), while a box WITH the
|
|
// engine and no ffmpeg reports it as blocked and names the missing piece —
|
|
// which is the sentence that tells an mp4 user what to install.
|
|
{Name: sttAVTool, Bin: sttBin, Args: []string{"--version"},
|
|
Needs: []prepTool{
|
|
{Name: "demux", Bin: "ffmpeg", Args: []string{"-version"}},
|
|
},
|
|
Prove: sttProve, Models: sttModelsReport},
|
|
}
|
|
|
|
// prepToolReport is one AVAILABLE tool as the host sees it.
|
|
type prepToolReport struct {
|
|
Name string `json:"name"`
|
|
Bin string `json:"bin"`
|
|
// Version is the tool's own first line, best-effort and for the operator's
|
|
// eyes only. Empty is NOT a failure signal — see the stream note above.
|
|
Version string `json:"version,omitempty"`
|
|
// Langs is what this job can read, when reading is language-shaped. Only OCR
|
|
// fills it in. It is informational: the box picks the language it uses and
|
|
// reports it in the job's result; this is so a user can see which packs are
|
|
// installed without running anything.
|
|
Langs []string `json:"langs,omitempty"`
|
|
// Models is what this job can load, best-first, when the job is one whose
|
|
// answer depends on a model. Only transcription fills it in.
|
|
//
|
|
// The FIRST entry is what runs when the host names none. The host may name
|
|
// one from this list in the job (ctxExtractReq.Model) and the reply says
|
|
// which one ACTUALLY ran (ctxExtractResp.Model), so a transcript's accuracy
|
|
// is a recorded fact rather than something inferred from whatever the machine
|
|
// happened to hold at the time.
|
|
Models []string `json:"models,omitempty"`
|
|
// Builtin says this job needs nothing installed — the binary does it. The
|
|
// host shows it so a user comparing two machines can see that one of these
|
|
// jobs is available on every box running this version and the others depend
|
|
// on what they have put on the machine.
|
|
Builtin bool `json:"builtin,omitempty"`
|
|
// CeilingS is the SECONDS this box will spend on this job before it stops,
|
|
// and unlike Langs it is NOT informational — the host's dispatch wait is
|
|
// built from it.
|
|
//
|
|
// THE NUMBER ORIGINATES HERE AND TRAVELS; there is no copy of it on the
|
|
// host. It was briefly a table on the box and a mirrored table on the host
|
|
// with a comment on each side asserting their relationship, which is the
|
|
// shape that had ALREADY failed once in this lane — the pull path's two
|
|
// deadline constants were in the wrong order across the whole domain while
|
|
// both comments claimed otherwise.
|
|
//
|
|
// The exposure a mirror leaves points the way its mitigation does not. A
|
|
// host's fallback for a job it has never heard of can only be the largest
|
|
// ceiling THE HOST knows. The bastion publishes separately and users update
|
|
// on their own schedule, so a box shipping a transcription job bounded at
|
|
// ten minutes — an hour of audio on a CPU is not a 150-second job — against
|
|
// a host that has not been updated gives: host waits 170 s, box works 600 s,
|
|
// and the user is told their machine did not answer by the side that quit
|
|
// first, with the transcript discarded when it arrives. That is exactly the
|
|
// failure the pull path was rebuilt to eliminate.
|
|
//
|
|
// So: the box declares what each job costs, the host waits that plus a round
|
|
// trip, and the host GRANTS the same number back in the job as
|
|
// extract_budget, which the box clamps DOWN to and never up — the shape
|
|
// pull_budget already proved one change earlier. A job that does not exist
|
|
// yet is covered by construction rather than by anyone remembering.
|
|
CeilingS float64 `json:"ceiling_s"`
|
|
}
|
|
|
|
// prepToolBlocked is a job whose PROGRAM is on this machine but which still
|
|
// cannot run, with the reason in plain language.
|
|
//
|
|
// This is the whole quiet-vs-blind distinction for tools. Without it a box with
|
|
// no OCR engine and a box with the engine and no language data are the same
|
|
// silence — the same empty tool list, the same "none of your machines can do
|
|
// this" — and the two are fixed by different commands. Reported for a tool
|
|
// whose FIRST binary answered, so a machine with none of the software installed
|
|
// stays quiet rather than listing everything it has never had.
|
|
type prepToolBlocked struct {
|
|
Name string `json:"name"`
|
|
Bin string `json:"bin"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// prepScanReport is how much was looked at, so "no tools" from a box that
|
|
// probed three binaries is visibly different from a box that probed none.
|
|
type prepScanReport struct {
|
|
// Supported is whether this build can probe at all. Always true here (the
|
|
// probe is exec, which every platform has); it exists so a future build
|
|
// that cannot probe on some platform has an honest way to say so, the way
|
|
// schedScanReport.Supported does.
|
|
Supported bool `json:"supported"`
|
|
// Probed is how many tools were tried, Found how many answered.
|
|
Probed int `json:"probed"`
|
|
Found int `json:"found"`
|
|
// Blocked is every job that is ALMOST here — its program is installed and
|
|
// something else is missing. See prepToolBlocked. An older host ignores the
|
|
// field; a newer one turns it into the sentence that tells the user what to
|
|
// install.
|
|
Blocked []prepToolBlocked `json:"blocked,omitempty"`
|
|
// Pull is whether this box can FETCH a source instead of being handed it in
|
|
// the management frame (contextpull.go). It rides the scan report, not
|
|
// prep_tools, because the transport is a property of the BOX's extraction
|
|
// lane and not of any one tool — every tool on a box that can pull can be
|
|
// fed by a pull.
|
|
//
|
|
// It is what keeps the host honest across versions: a bastion that predates
|
|
// this reports no field, the host reads that as false, and it keeps sending
|
|
// documents inline instead of handing out a ticket that box could not
|
|
// redeem. Advertising it means "this box COULD fetch", never "will" — the
|
|
// host still decides, per job, which transport to use.
|
|
Pull bool `json:"pull"`
|
|
}
|
|
|
|
var prepCache = struct {
|
|
sync.Mutex
|
|
at time.Time
|
|
tools []prepToolReport
|
|
scan prepScanReport
|
|
}{}
|
|
|
|
// probePrepTool runs one tool's proving argv. Returns whether it ran, and its
|
|
// first output line if there was one.
|
|
//
|
|
// Both streams are read because the banner's stream varies by tool (measured
|
|
// above). The exit code is the ONLY input to the availability decision — a
|
|
// tool that runs but prints nothing is available.
|
|
func probePrepTool(t prepTool) (bool, string) {
|
|
ok, ver, _ := probePrepToolDetail(t)
|
|
return ok, ver
|
|
}
|
|
|
|
// probePrepToolDetail is the full answer: available, its version, and — when it
|
|
// is NOT available but its program is installed — why not.
|
|
//
|
|
// The order is deliberate and each step is a strictly better authority than the
|
|
// one before it: the program runs, then everything it depends on runs, then the
|
|
// job is actually PERFORMED on a known input. A tool with no Prove stops after
|
|
// the first two, which is right for pdftotext (there is no second thing it could
|
|
// be missing) and wrong for tesseract (there is, and it exits 0 anyway).
|
|
func probePrepToolDetail(t prepTool) (bool, string, string) {
|
|
if t.Builtin {
|
|
// No program, so the first two steps have nothing to ask. The THIRD one
|
|
// still runs and is the whole probe: Prove performs the job on a known
|
|
// input and compares the answer. A builtin is never "not installed", but
|
|
// it can still be broken, and a broken one must not be advertised.
|
|
if t.Prove != nil {
|
|
if ok, why := t.Prove(); !ok {
|
|
return false, "", why
|
|
}
|
|
}
|
|
return true, "built in — nothing to install", ""
|
|
}
|
|
out, err := runCmd(5*time.Second, t.Bin, t.Args...)
|
|
if err != nil {
|
|
// A non-zero exit and a missing binary both land here, and they are NOT
|
|
// the same thing to the user. An absent program is quiet — there is
|
|
// nothing to say about software nobody installed. A present one that
|
|
// will not run is named.
|
|
if _, lerr := exec.LookPath(t.Bin); lerr == nil {
|
|
return false, "", t.Bin + " is installed on this machine but did not run"
|
|
}
|
|
return false, "", ""
|
|
}
|
|
for _, n := range t.Needs {
|
|
if _, nerr := runCmd(5*time.Second, n.Bin, n.Args...); nerr != nil {
|
|
return false, "", t.Bin + " works but " + n.Bin + " is missing or will not run, " +
|
|
"and this job needs both"
|
|
}
|
|
}
|
|
if t.Prove != nil {
|
|
if ok, why := t.Prove(); !ok {
|
|
return false, "", why
|
|
}
|
|
}
|
|
// Success: runCmd gave clean stdout, which is EMPTY for tools that banner
|
|
// to stderr. Absent output is not absent capability, so report available
|
|
// with no version rather than probing again for a cosmetic string.
|
|
return true, firstLine(out), ""
|
|
}
|
|
|
|
// firstLine is the tool's own first line of output, trimmed and length-capped
|
|
// so a chatty or hostile binary cannot inflate the heartbeat body.
|
|
func firstLine(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if i := strings.IndexAny(s, "\r\n"); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
if len(s) > 120 {
|
|
s = s[:120]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// scanPrepTools is the heartbeat's entry point: probe every known tool, cached.
|
|
func scanPrepTools() ([]prepToolReport, prepScanReport) {
|
|
prepCache.Lock()
|
|
defer prepCache.Unlock()
|
|
if !prepCache.at.IsZero() && time.Since(prepCache.at) < prepCacheFor {
|
|
// Tools are cached; the transport is not. It flips exactly once, when
|
|
// the daemon wires its tunnel client, and a box that probed before that
|
|
// would otherwise keep reporting pull=false for up to prepCacheFor —
|
|
// telling the host to send documents inline on a machine that can fetch.
|
|
scan := prepCache.scan
|
|
scan.Pull = ctxPullConfigured()
|
|
return prepCache.tools, scan
|
|
}
|
|
tools := []prepToolReport{}
|
|
blocked := []prepToolBlocked{}
|
|
for _, t := range prepToolTable {
|
|
ok, ver, why := probePrepToolDetail(t)
|
|
if !ok {
|
|
if why != "" {
|
|
blocked = append(blocked, prepToolBlocked{Name: t.Name, Bin: t.Bin, Reason: why})
|
|
}
|
|
continue
|
|
}
|
|
// The ceiling is read from the SAME function the job is actually run
|
|
// under (ctxExtractTimeoutFor), never from a second literal beside it —
|
|
// the declaration and the enforcement have to be one value or the
|
|
// declaration is just another mirror, one file further away.
|
|
r := prepToolReport{Name: t.Name, Bin: t.Bin, Version: ver,
|
|
Builtin: t.Builtin,
|
|
CeilingS: ctxExtractTimeoutFor(t.Name).Seconds()}
|
|
if t.Langs != nil {
|
|
r.Langs = t.Langs()
|
|
}
|
|
if t.Models != nil {
|
|
r.Models = t.Models()
|
|
}
|
|
tools = append(tools, r)
|
|
}
|
|
// Pull is read LIVE, not cached with the tool probes: it flips once when the
|
|
// daemon's tunnel client is wired (run.go), and a box that reported false in
|
|
// the first beat must not keep saying so for prepCacheFor afterwards.
|
|
scan := prepScanReport{Supported: true, Probed: len(prepToolTable), Found: len(tools),
|
|
Blocked: blocked, Pull: ctxPullConfigured()}
|
|
prepCache.at, prepCache.tools, prepCache.scan = time.Now(), tools, scan
|
|
return tools, scan
|
|
}
|
|
|
|
// prepCapable says whether the capability travels for THIS pairing.
|
|
//
|
|
// Two conditions, both required, and the caps one is not optional politeness:
|
|
// on an anchored box a member pairing without the dcn family will be refused
|
|
// at the op, so advertising to it would be a control that fails after the user
|
|
// has already chosen it — the reason mirror.go:63-67 gives for gating its own
|
|
// advertisement. Withheld means the probe is skipped entirely, matching how
|
|
// the inference lanes behave at agents.go:351-358.
|
|
func prepCapable(self string) ([]prepToolReport, prepScanReport, bool) {
|
|
if capsAllow(self, prepGateOp) != "" {
|
|
return nil, prepScanReport{}, false
|
|
}
|
|
tools, scan := scanPrepTools()
|
|
return tools, scan, len(tools) > 0
|
|
}
|