@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.10.2",
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",
@@ -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 createHash(DIGEST_ALGORITHM).update(bytes).digest(DIGEST_ENCODING);
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 === recorded) return [];
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
  }