← Documentation index Architecture guide › Vaglio

Metnos

Vaglio: the check before an action
The active safeguard, experimental APIs, and limits you should know.

Once Metnos has selected an executor and prepared its arguments, it performs one last local check before starting it. This check, known as the Vaglio guard, recognises explicitly forbidden paths and commands. The code also contains a scoring judge, but that judge is not part of the normal execution path today.

Contents

  1. What Vaglio does
  2. What the guard checks
  3. A concrete example
  4. The complete judgement API
  5. The two scoring judges
  6. How it is wired into the runtime
  7. Logs and retained data
  8. Guarantees and limitations
  9. Configuration and references

1. What Vaglio does

The guard receives the executor name and its final arguments. Its answer is binary: pass or block. If it blocks, the runtime does not invoke the executor and stops the plan.

Its responsibility is deliberately narrow. Vaglio does not choose the executor, decide whether the request is useful, grant permission, or replace policy, human consent, the sandbox, or the executor's own checks. These are separate safeguards and must remain independent.

Part of the moduleUse in the normal runtime
Deterministic guardActive before invocation.
Scoring judgeAvailable as a separate API, but not wired into the normal dispatcher.

2. What the guard checks

guard_check(executor_name, args, context) walks nested arguments and looks for strings that may identify a destination or resource. It ignores values under fields that explicitly carry text, such as content, body, comment, and message: a document that mentions ~/.ssh is not necessarily trying to read it.

CheckWhat it blocks
Always-forbidden pathsAmong others: ~/.ssh, .gnupg, common AWS credential files, /etc/passwd, /etc/shadow, /etc/ssh, /root, /boot, parts of /proc, and block devices.
Changes to system treesWrites, moves, creations, and deletions under directories protected by the host operating system, such as /etc and /usr on Linux. This rule does not block reads.
Nearly irreversible shell commandsA closed set of patterns, including formatting a filesystem, writing directly to a device, recursively deleting the root, and a fork bomb.

The system-tree check recognises mutating actions from the canonical prefix of the executor name, such as write_, delete_, or move_. The shell check always recognises the name shell_exec; it can also use capability=code:exec when a caller provides that context. The normal runtime wiring currently supplies only the name and arguments.

When no rule is violated, the guard returns (True, None). This means only that none of its known prohibitions matched, not that the action is guaranteed to be harmless.

3. A concrete example

The request “Read /etc/hosts and show me the uncommented lines” may pass: /etc/hosts is not on the always-forbidden list, and read_files does not change the system.

If the request becomes “Replace /etc/hosts with this content” and the runtime prepares write_files, the combination of a mutating action and a protected path is blocked before invocation. Merely reading /etc/shadow, however, is also stopped because that file is on the always-forbidden list.

4. The complete judgement API

The module also exposes judge(intent, executor_name, args, context). This function runs the guard first and, if the guard passes the action, returns a complete Verdict:

FieldMeaning
approvedOverall outcome of the call.
reasonTechnical reason produced by the module.
tsUnix date and time of the decision.
judge_kindrule-based-v1, llm-v1, or safe-verb-shortcut.
scoreScore from 0 to 1; it is 0 when the guard blocks.
blocked_byguard, judge, or no value when the call approves.

Read-only and compute verbs listed in SAFE_VERBS, such as read, find, and compare, are approved after the guard without calling the scoring judge. Their result uses judge_kind=safe-verb-shortcut.

5. The two scoring judges

rule-based-v1. This is the default backend for the judge() API. It starts at 0.7, adds a small bonus when the request contains part of the executor name, and applies penalties for a possible directory traversal or unusual argument keys. With the penalties currently implemented, the minimum score is 0.4; the default threshold is 0.30. Under this configuration the judge can flag anomalies, but it cannot reject an action that has already passed the guard.

llm-v1. This optional backend uses the vaglio.judge workload, currently assigned to the middle tier. It receives the intent, executor name, argument-key names, and a few context fields; it does not receive argument values. The prompt template is selected in the active language, but the text that presents these values still uses Italian labels, so this internal API is not yet fully aligned with the i18n structure.

If the router, model call, or response parsing fails, llm-v1 returns 0.5. The default threshold approves that fallback. The judge is therefore an experimental heuristic, not a fail-closed security control or proof that an action serves the user's ends.

6. How it is wired into the runtime

The dispatcher passes guard_check to the shared engine as vaglio_guard. The engine uses it during the preflight for parallel reads and, for ordinary steps, immediately before invoking the executor. A refusal produces the vaglio_guard error class and stops the plan without running that step.

The engine also accepts a callback named vaglio_judge, but the production dispatcher does not provide it. Moreover, the current extension point runs after a step has returned: if wired as it stands, it could stop later steps but could not prevent the effect that has just occurred. It must not be described as a second preventive check.

The same module contains check_cross_user_send(), which permits host sends and self-sends and rejects the rest. This is a helper available to callers, not evidence that every messaging path invokes it.

7. Logs and retained data

Each call to judge() appends a JSONL record to a monthly file in the user's vaglio/ directory. The record contains the verdict, intent, executor name, argument-key names, and context-key names. It does not contain argument values. The intent may still carry personal information, so the log must be treated as confidential data.

The guard used directly by the normal runtime does not write these records: a block appears in the step result and engine logs. If writing the monthly log fails, judge() still keeps its verdict.

8. Guarantees and limitations

9. Configuration and references

SettingDefaultEffect
METNOS_JUDGE_KINDrule-based-v1Backend used only by callers of judge().
METNOS_JUDGE_THRESHOLD0.30Threshold applied by the scoring judge.