@synovitec/trellis 0.6.0-rc.2 → 0.6.0-rc.3
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/CHANGELOG.md +13 -0
- package/SECURITY.md +2 -1
- package/package.json +1 -1
- package/src/commands/attest.mjs +4 -3
- package/src/commands/init.mjs +3 -1
- package/src/commands/verify.mjs +4 -3
- package/src/verify/rules.mjs +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this package. The format follows Keep a Changelog; versio
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0-rc.3] - 2026-10-31
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The commit checkpoint is installed without gitleaks.** `init --agents` wrote the git hooks only
|
|
12
|
+
where gitleaks was on, so a machine without it got no `trellis verify` on commit and no
|
|
13
|
+
attestation at all, and said nothing. The hooks are now always written, and init says the secret
|
|
14
|
+
scan is off and how to add it.
|
|
15
|
+
- **A secret scan that could not run warns on this machine where the team did not switch gitleaks
|
|
16
|
+
on**, rather than refusing every commit; where `"gitleaks": true` is set, or the team set
|
|
17
|
+
`secrets-scanned` itself, it still blocks. CI, which brings its own pinned gitleaks, blocks as
|
|
18
|
+
before, and a leak found is never softened.
|
|
19
|
+
|
|
7
20
|
## [0.6.0-rc.2] - 2026-10-30
|
|
8
21
|
|
|
9
22
|
The tenth review, on the code the ninth round added, found the secret scan's own reading of team
|
package/SECURITY.md
CHANGED
|
@@ -34,7 +34,8 @@ is a seatbelt for an agent that cuts corners, not a sandbox for one that attacks
|
|
|
34
34
|
does, after quotes and escapes;
|
|
35
35
|
- every check fails closed: a log that does not verify, a scanner that could not run, a registry
|
|
36
36
|
that did not answer, a broken config, a hook whose Trellis is gone (it exits 2) is a finding,
|
|
37
|
-
never a pass
|
|
37
|
+
never a pass. One is a warning rather than a block: a secret scan that could not run on a
|
|
38
|
+
developer's machine where the team did not switch gitleaks on; CI, with its own gitleaks, blocks;
|
|
38
39
|
- the attestation of each commit is signed with the developer's ed25519 key.
|
|
39
40
|
|
|
40
41
|
An agent determined to forge evidence with the user's own permissions can still do it: read the key
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synovitec/trellis",
|
|
3
|
-
"version": "0.6.0-rc.
|
|
3
|
+
"version": "0.6.0-rc.3",
|
|
4
4
|
"description": "Proof that a change got the checks your repository asks for, whoever made it: AI coding agents (Claude Code, Cursor, Codex, Copilot, any MCP agent) and people, held at the commit and verified again in CI with signed in-toto attestations.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/commands/attest.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { buildStatement, existingPublicKeyPem, keyIdOf, readNotes, signEnvelope,
|
|
|
13
13
|
import { loadConfig, validateConfig } from "../core/config.mjs";
|
|
14
14
|
import { scanCommit } from "../verify/secrets.mjs";
|
|
15
15
|
import { checkDependencies, dependencyChanges } from "../verify/deps.mjs";
|
|
16
|
-
import { evaluate } from "../verify/rules.mjs";
|
|
16
|
+
import { evaluate, localModes } from "../verify/rules.mjs";
|
|
17
17
|
import { testCommands, testScriptChanges } from "../verify/test-commands.mjs";
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -76,13 +76,14 @@ async function judgeCommit(ctx, commit, tree, events, env, opts) {
|
|
|
76
76
|
if (errors.length) return { actor, agent, preset, findings: [{ rule: "config-invalid", mode: "block", message: errors.join("; ") }], verdict: "fail" };
|
|
77
77
|
const changes = commitDiff(ctx.root, commit);
|
|
78
78
|
const deps = dependencyChanges(ctx.root, changes);
|
|
79
|
+
const secrets = (opts.scanCommit ?? scanCommit)(ctx.root, commit);
|
|
79
80
|
const findings = evaluate({
|
|
80
81
|
changes,
|
|
81
82
|
events,
|
|
82
83
|
actor,
|
|
83
84
|
preset,
|
|
84
|
-
modes: config
|
|
85
|
-
secrets
|
|
85
|
+
modes: localModes(config, secrets),
|
|
86
|
+
secrets,
|
|
86
87
|
deps: deps.length ? await checkDependencies(deps, { http: opts.http }) : { status: "clean", findings: [], unpinned: [], unchecked: [] },
|
|
87
88
|
depNames: deps.map((d) => d.name),
|
|
88
89
|
testCommands: testCommands((p) => ctx.read(p)),
|
package/src/commands/init.mjs
CHANGED
|
@@ -90,7 +90,9 @@ function planAgentHooks(ctx, enabled, writes, notes, agentList) {
|
|
|
90
90
|
].filter(Boolean);
|
|
91
91
|
writes.push({ path: SETTINGS, text: settings.text, why: what.join("; ") });
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
// The commit checkpoint holds with or without gitleaks: without it, commits are not scanned for
|
|
94
|
+
// secrets here (CI scans them), and the note says how to add it.
|
|
95
|
+
if (!enabled.some((e) => e.tool.id === "gitleaks")) notes.push('gitleaks is off, so commits are not scanned for secrets on this machine (CI scans them): install it from github.com/gitleaks/gitleaks, then set "gitleaks": true in trellis.config.json');
|
|
94
96
|
const git = planGitHooks(ctx);
|
|
95
97
|
for (const w of git.writes) writes.push({ ...w, executable: true });
|
|
96
98
|
notes.push(...git.notes);
|
package/src/commands/verify.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { loadConfig, validateConfig } from "../core/config.mjs";
|
|
|
19
19
|
import { parseJson } from "../core/context.mjs";
|
|
20
20
|
import { scanChanges, scanCommit as scanCommitSecrets, scanStaged } from "../verify/secrets.mjs";
|
|
21
21
|
import { checkDependencies, dependencyChanges } from "../verify/deps.mjs";
|
|
22
|
-
import { evaluate } from "../verify/rules.mjs";
|
|
22
|
+
import { evaluate, localModes } from "../verify/rules.mjs";
|
|
23
23
|
import { isTestCommand, RUNNER_SETTINGS, testCommands, testEnv, testScriptChanges } from "../verify/test-commands.mjs";
|
|
24
24
|
import { matchesAny } from "../agents/glob.mjs";
|
|
25
25
|
import { tmpdir } from "node:os";
|
|
@@ -88,13 +88,14 @@ export async function verify(ctx, out, opts = {}) {
|
|
|
88
88
|
const deps = dependencyChanges(ctx.root, changes);
|
|
89
89
|
const agent = agentAtWork(window.events, env);
|
|
90
90
|
const preset = config?.verify?.preset ?? "recommended";
|
|
91
|
+
const secrets = (opts.scanSecrets ?? scanStaged)(ctx.root);
|
|
91
92
|
const findings = evaluate({
|
|
92
93
|
changes,
|
|
93
94
|
events: window.events,
|
|
94
95
|
actor: agent ? "agent" : "person",
|
|
95
96
|
preset,
|
|
96
|
-
modes: config
|
|
97
|
-
secrets
|
|
97
|
+
modes: localModes(config, secrets),
|
|
98
|
+
secrets,
|
|
98
99
|
deps: deps.length ? await checkDependencies(deps, { http: opts.http }) : { status: "clean", findings: [], unpinned: [], unchecked: [] },
|
|
99
100
|
depNames: deps.map((d) => d.name),
|
|
100
101
|
testCommands: testCommands((p) => ctx.read(p)),
|
package/src/verify/rules.mjs
CHANGED
|
@@ -89,6 +89,20 @@ export const TEST_PATHS = Object.freeze([...TEST_FILES, ...GATES]);
|
|
|
89
89
|
/** @param {string[]} paths */
|
|
90
90
|
const list = (paths) => `${paths.slice(0, 6).join(", ")}${paths.length > 6 ? `, and ${paths.length - 6} more` : ""}`;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The rule modes a commit on this machine is judged under: the team's, except that a secret scan
|
|
94
|
+
* that could not run warns where the team did not switch gitleaks on (and did not set the rule).
|
|
95
|
+
* CI, which brings its own gitleaks, still blocks.
|
|
96
|
+
* @param {any} config trellis.config.json, or null
|
|
97
|
+
* @param {import("./secrets.mjs").SecretScan} secrets
|
|
98
|
+
* @returns {Record<string, Mode>}
|
|
99
|
+
*/
|
|
100
|
+
export function localModes(config, secrets) {
|
|
101
|
+
const modes = { ...(config?.verify?.rules ?? {}) };
|
|
102
|
+
if (secrets.status === "unavailable" && !config?.tools?.gitleaks && modes["secrets-scanned"] === undefined) modes["secrets-scanned"] = "warn";
|
|
103
|
+
return modes;
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
/**
|
|
93
107
|
* @param {object} input
|
|
94
108
|
* @param {import("../core/diff.mjs").Change[]} input.changes
|