@agentsfleet/orly 0.10.2 → 0.10.4
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/package.json +1 -1
- package/src/config.test.ts +18 -0
- package/src/config.ts +20 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/config.test.ts
CHANGED
|
@@ -180,6 +180,24 @@ describe("managedDrift — content", () => {
|
|
|
180
180
|
expect(drift[0]).toContain("move the change into the pack source");
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
test("a digest is algorithm-prefixed, and the unprefixed spelling still matches", async () => {
|
|
184
|
+
// Bare hex is indistinguishable from a credential to an entropy scanner:
|
|
185
|
+
// gitleaks refused the first commit that carried these, in every consuming
|
|
186
|
+
// repository at once. The prefix is what makes it pass without a scanner
|
|
187
|
+
// suppression — and 0.10.2's unprefixed records must not read as drift.
|
|
188
|
+
const bytes = new TextEncoder().encode("managed\n");
|
|
189
|
+
const digest = contentDigest(bytes);
|
|
190
|
+
expect(digest.startsWith("sha256:")).toBe(true);
|
|
191
|
+
|
|
192
|
+
const root = scratch();
|
|
193
|
+
writeFileSync(join(root, "managed.md"), "managed\n");
|
|
194
|
+
const legacy = { ...(await seedConfig(root)), managed: ["managed.md"], digests: { "managed.md": digest.slice("sha256:".length) } };
|
|
195
|
+
expect(managedDrift(root, legacy)).toEqual([]);
|
|
196
|
+
|
|
197
|
+
const prefixed = { ...legacy, digests: { "managed.md": digest } };
|
|
198
|
+
expect(managedDrift(root, prefixed)).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
183
201
|
test("a file with no recorded digest makes no claim", async () => {
|
|
184
202
|
// Two cases share this path: a config written before digests existed, and a
|
|
185
203
|
// hook orly manages but did not author. Neither may be reported as drift.
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
3
2
|
import { dirname, extname, join, relative } from "node:path";
|
|
4
3
|
|
|
5
|
-
import { assertWritableInside, isObject, isString, JsonObject, objectValue, OrlyError, readJsonObject, RulesModel, stringArray } from "./model";
|
|
4
|
+
import { assertWritableInside, hashContent, isObject, isString, JsonObject, objectValue, OrlyError, readJsonObject, RulesModel, stringArray } from "./model";
|
|
6
5
|
import { UNSCOPED_ENVIRONMENT } from "./git_env";
|
|
7
6
|
import { validateCommands, validateSurfaces } from "./validation";
|
|
8
7
|
|
|
@@ -19,7 +18,6 @@ const VERSION_FIELD = "orly_version";
|
|
|
19
18
|
const MANAGED_FIELD = "managed";
|
|
20
19
|
const DIGESTS_FIELD = "digests";
|
|
21
20
|
const DIGEST_ALGORITHM = "sha256";
|
|
22
|
-
const DIGEST_ENCODING = "hex";
|
|
23
21
|
const COMMANDS_FIELD = "commands";
|
|
24
22
|
const CONFORM_COMMAND = "conform";
|
|
25
23
|
const MAKE_COMMAND = "make";
|
|
@@ -130,8 +128,25 @@ function readDigests(value: unknown): Record<string, string> {
|
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
/// The digest orly records for content it wrote, and recomputes to check it.
|
|
131
|
+
///
|
|
132
|
+
/// Algorithm-prefixed, and not for elegance: a bare 64-character hex string is
|
|
133
|
+
/// indistinguishable from a credential to an entropy-based secret scanner, and
|
|
134
|
+
/// gitleaks refused the very first commit that carried these — in EVERY
|
|
135
|
+
/// consuming repository at once, since `orly update` writes one per managed
|
|
136
|
+
/// file. The prefix is what makes the value self-describing enough to pass,
|
|
137
|
+
/// and the alternative was a scanner suppression, which is how a real
|
|
138
|
+
/// credential eventually rides through.
|
|
133
139
|
export function contentDigest(bytes: Uint8Array): string {
|
|
134
|
-
return
|
|
140
|
+
return `${DIGEST_ALGORITHM}:${hashContent(bytes)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Tolerates the unprefixed spelling 0.10.2 wrote for its one release, so a
|
|
144
|
+
// checkout installed on that version is not reported as wholly drifted before
|
|
145
|
+
// its next update — a format change is not an edit, and saying so would be the
|
|
146
|
+
// false alarm that teaches people to ignore the real ones.
|
|
147
|
+
function sameDigest(recorded: string, actual: string): boolean {
|
|
148
|
+
const bare = (digest: string) => digest.startsWith(`${DIGEST_ALGORITHM}:`) ? digest.slice(DIGEST_ALGORITHM.length + 1) : digest;
|
|
149
|
+
return bare(recorded) === bare(actual);
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
// What orly installed here, and whether the engine has moved since.
|
|
@@ -196,7 +211,7 @@ function editedManaged(targetRoot: string, config: RepoConfig, present: string[]
|
|
|
196
211
|
const recorded = config.digests[relativePath];
|
|
197
212
|
if (!recorded) return [];
|
|
198
213
|
const actual = contentDigest(readFileSync(join(targetRoot, relativePath)));
|
|
199
|
-
if (actual
|
|
214
|
+
if (sameDigest(recorded, actual)) return [];
|
|
200
215
|
return [`managed file was edited after orly wrote it: ${relativePath} — move the change into the pack source, or \`orly update\` to discard it`];
|
|
201
216
|
});
|
|
202
217
|
}
|