feat(evaluate): kosli evaluate policy, evaluating and recording a decision in one request - #1201
Conversation
Implements the planning step for kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Evaluate a policy against a trail where the trail is stored, and print the verdict. The request-and-verdict sequence is extracted from the hidden --server-side path so both commands share it. No decision is recorded yet, and asserting arrives with --assert in the next slice. Slice 1 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exit with a non-zero status when the policy denies. Asserting is opt-in here, the reverse of the other evaluate commands: recording a decision is not a reason to fail the step that asked for it. An evaluation that has not answered fails either way and is never reported as a verdict. Slice 2 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--context trail=<flow>/<trail>, repeated once per trail, is now the only way to say what kosli evaluate policy evaluates, and it is required. --flow and --trail return in the next slice as the decision's destination alone: a pipeline sets them as environment variables for every command, so their presence must not fail a run that asked for no decision. Slice 3 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--control sends a decision block with the evaluation, so the outcome is recorded where the policy runs and no verdict is asserted from here. The name defaults to <control>-decision, and --flow and --trail name the destination alone -- satisfied by KOSLI_FLOW and KOSLI_TRAIL, and never refused for being present without a control. The written decision's id is printed with the verdict. Slice 4 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A refusal reaches the caller as the status the API answered with and its own message, and a policy that could not run is never worded as a denial. The shared evaluation path already does this; these tests pin it for kosli evaluate policy, so no list of server cases has to be kept in step here. Slice 5 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--policy may name a directory, and every file below it travels keyed by its path relative to that directory. Nothing is left out by name: what a bundle may hold, and what its modules may import, is for the evaluator that runs it to judge. The published file and byte caps, and an empty directory, are still named here rather than sent to be refused. A policy now comes from this machine only. Fetching one from a URL is on its way out, so this command does not offer it. Slice 6 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The command is in no help listing and gets no docs page, though it runs for anyone who names it. Nothing about it can be exercised end to end until it runs against a server that can evaluate, and a listed command is a contract from the moment it ships. The ticket asks for a published command; unhiding it is one line, and it belongs with the wrap-up. Slice 6 of kosli-dev/server#6920. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @jumboduck's task in 1m 34s —— View job Follow-up review of commit
|
| # | Finding | Status |
|---|---|---|
| 1 | --fingerprint not validated before send (evaluatePolicy.go) |
✅ Resolved — digest.ValidateDigest is now called in decision() before the evaluations.Decision is built (cmd/kosli/evaluatePolicy.go:194-198), with a new test case ("a fingerprint that is not a SHA256") asserting "not a valid SHA256" and that nothing is sent. |
| 2 | Symlinks followed / caps checked after full read (evaluateHelpers.go) | evaluateHelpers.go at all. The thread shows resolved on GitHub, but the walk still uses os.ReadFile on every non-dir DirEntry (no entry.Type().IsRegular() check) and still accumulates all files before checking maxPolicyBundleFiles/serverPolicyMaxBytes. Worth re-opening or explicitly deferring. |
| 3 | Refusal names only the flag that was typed (evaluatePolicy.go:172) | ✅ Resolved — new namedDecisionFlags(name, fingerprint) helper builds the message from what was actually given; tests now assert both the mentioned flag and saysNot for the flag not given, plus a combined-both case. |
| 4 | --output validated too late, after the decision request (evaluatePolicy.go:161) |
✅ Resolved — run() now rejects an unknown --output value against evaluatePolicyOutputs ({"table": true, "json": true}) before parseTrailContexts/decision()/runServerEvaluation run, so a bad format never reaches the API. Matches the {"json", "table"} set actually wired in runServerEvaluation's output.FormattedPrint call, so no drift. New test: "an output format that does not exist". |
New issues in the changed lines: none. The added validation is placed correctly (after the --flow/--trail presence check, before the fingerprint is put on the wire), and the new test cases match the behavior.
3 of 4 findings resolved; the symlink/streaming-caps finding in evaluateHelpers.go remains open and untouched by this commit.
· branch 6920-evaluate-policy
|
@jumboduck could |
@mbevc1 Not at the moment. We might have a better idea after Malmö. |
A malformed fingerprint and an unknown output format are now refused before the evaluation is created, and a decision flag without --control is answered with the flag that was typed. The output format matters more here than on the other evaluate commands: checked where it prints, a typo would record the decision and then fail, and a rerun would record a second one. Raised in review on #1201. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gotcha, I was just wondering how come parameter is not just called |
@mbevc1 --trail is already used for the target of the decision attestation. You could evaluate one or several trails against a policy, and attest the decision on a completely different trail. |
Implements kosli-dev/server#6920.
A new command evaluates a policy where the trail is stored and records the outcome as a decision in the same request. The decision value never passes through the CLI, which is what removes the self-reported step this replaces: evaluate, read
allowout of a local report, then assert it back withkosli attest decision --compliant=<value>.kosli evaluate policy \ --context trail=my-release-flow/$GITHUB_SHA \ --policy ./policies/SDLC-CTRL-0007-code-review \ --control SDLC-CTRL-0007 \ --fingerprint "$ARTIFACT_FINGERPRINT" \ --params '{"protected_branch": "master"}' \ --assertWhat it does
--context trail=<flow>/<trail>, required and repeatable, names what is evaluated. Every trail travels in one evaluation, so they resolve at one instant.--controlrecords the outcome as a decision in--flow/--trail, under--name(default<control>-decision), about--fingerprintor about the trail itself. Without--controlnothing is recorded, and the destination flags are not required.--assertexits non-zero on a denial. Without it the verdict prints and the command exits 0: recording a decision is not a reason to fail the step that asked for it.--policytakes a file or a directory. A directory travels as one bundle keyed by paths relative to it, with the published 100-file and 1 MiB caps refused here rather than sent to be rejected.kosli evaluate trail, table and JSON, plus the written decision's id where there is one.Decisions worth a reviewer's attention
--flowand--trailname only the destination, are satisfied byKOSLI_FLOWandKOSLI_TRAIL, and are never refused for being present without--control— a pipeline sets them for every command it runs.--syncflag.http(s)://policies on this command, though the older evaluate commands keep them: that way of naming a policy is on its way out.The request-and-verdict sequence is shared with the hidden
--server-sideflag onevaluate trail, so the two cannot drift.Testing
make lint: 0 issues.Plan and handover:
docs/plans/6920-evaluate-policy.md,docs/handover/6920-evaluate-an-inline-policy-and-record-its-decis.md.🤖 Generated with Claude Code