@entrorove/skill 0.1.61 → 0.1.64

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.
Files changed (54) hide show
  1. package/CAPABILITY-MAP.md +7 -2
  2. package/CHANGELOG.md +15 -0
  3. package/IDE-EXECUTION.md +39 -6
  4. package/REUSE-DISCIPLINE.md +1 -1
  5. package/SKILL.md +6 -2
  6. package/cli-input.mjs +22 -0
  7. package/cli.mjs +2 -8
  8. package/cloud-task-client.mjs +10 -2
  9. package/cloud-task-images.mjs +39 -0
  10. package/ide-exit.mjs +49 -9
  11. package/ide-hook.mjs +2 -2
  12. package/local-contracts.mjs +4 -1
  13. package/local-discipline-gate.mjs +14 -3
  14. package/local-discipline-repository.mjs +11 -5
  15. package/local-finder-plist.mjs +87 -0
  16. package/local-finder-store.mjs +115 -0
  17. package/local-mutation-authority.mjs +11 -0
  18. package/local-mutation-completion-evidence.mjs +83 -0
  19. package/local-mutation-consumption.mjs +39 -14
  20. package/local-mutation-environment-observation.mjs +85 -0
  21. package/local-mutation-finder-observation.mjs +58 -0
  22. package/local-mutation-wire.mjs +28 -9
  23. package/local-preserved-mutation-protection.mjs +8 -1
  24. package/local-preserved-mutation-wire.mjs +7 -3
  25. package/local-scope-files.mjs +3 -1
  26. package/local-scope-font.mjs +45 -0
  27. package/local-scope-repository.mjs +16 -3
  28. package/local-scope-zip.mjs +93 -0
  29. package/manifest.json +1 -1
  30. package/member-producer-repair-authority.mjs +48 -0
  31. package/model-images.d.mts +9 -0
  32. package/model-images.mjs +87 -0
  33. package/mutation-challenge-refresh.mjs +51 -0
  34. package/mutation-client.mjs +45 -27
  35. package/mutation-connection-state.mjs +17 -0
  36. package/package.json +18 -2
  37. package/release-state.json +52 -36
  38. package/release.mjs +12 -4
  39. package/releases.json +18 -0
  40. package/remote-client.mjs +1 -1
  41. package/remote-transport.mjs +48 -16
  42. package/scope-amendment-client.mjs +1 -1
  43. package/scope-amendment-consumption.mjs +32 -0
  44. package/scope-amendment-mutation.mjs +3 -4
  45. package/scope-amendment-observer.mjs +7 -3
  46. package/scope-amendment-remote.mjs +12 -2
  47. package/scope-amendment-retirement.mjs +54 -0
  48. package/scope-amendment-state.mjs +7 -4
  49. package/scope-amendment-wire.mjs +3 -2
  50. package/task-entry-client.mjs +1 -0
  51. package/workflow-observation-client.mjs +11 -0
  52. package/workflow-observation-connection.mjs +2 -1
  53. package/workflow-observation-remote.mjs +26 -1
  54. package/workflow-observation-wire.mjs +6 -3
@@ -39,6 +39,21 @@ function pinnedAuthority() {
39
39
  return { key, keyId: sha256(der) };
40
40
  } catch { mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Pinned cloud mutation authority is unavailable or invalid'); }
41
41
  }
42
+ function originalReceiver(payload) {
43
+ const receiver = payload.receiver;
44
+ const original = receiver?.schemaVersion === 'entrorove.original-task-mutation-receiver/1.0';
45
+ exact(receiver, original ? ['schemaVersion', 'taskId', 'ownerId', 'flowId', 'originalRequestId', 'taskDigest',
46
+ 'routeDigest', 'acceptanceReceiptId', 'acceptanceDigest']
47
+ : ['taskId', 'ownerId', 'projectId', 'hostId', 'threadId', 'descriptionDigest', 'taskDigest']);
48
+ if (receiver.taskId !== payload.taskId || receiver.ownerId !== payload.ownerId || receiver.taskDigest !== payload.taskDigest
49
+ || sha256(canonical(receiver)) !== payload.receiverDigest || (original
50
+ ? typeof receiver.originalRequestId !== 'string' || !ID.test(receiver.originalRequestId)
51
+ || ['flowId', 'routeDigest', 'acceptanceReceiptId', 'acceptanceDigest']
52
+ .some(field => receiver[field] !== payload[field])
53
+ : !SHA.test(receiver.descriptionDigest) || ['projectId', 'hostId', 'threadId'].some(field => !ID.test(String(receiver[field]))))) {
54
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority lost its original accepted receiver');
55
+ }
56
+ }
42
57
  function identity(payload) {
43
58
  if (Object.hasOwn(payload, 'scopeRevisionDigest') && !SHA.test(payload.scopeRevisionDigest)) {
44
59
  mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority scope revision is invalid');
@@ -74,13 +89,7 @@ function identity(payload) {
74
89
  || canonical([...payload.newFilePaths].sort()) !== canonical(payload.newFilePaths)) {
75
90
  mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority path set is invalid');
76
91
  }
77
- exact(payload.receiver, ['taskId', 'ownerId', 'projectId', 'hostId', 'threadId', 'descriptionDigest', 'taskDigest']);
78
- if (payload.receiver.taskId !== payload.taskId || payload.receiver.ownerId !== payload.ownerId
79
- || payload.receiver.taskDigest !== payload.taskDigest || !SHA.test(payload.receiver.descriptionDigest)
80
- || sha256(canonical(payload.receiver)) !== payload.receiverDigest
81
- || ['projectId', 'hostId', 'threadId'].some(field => !ID.test(String(payload.receiver[field])))) {
82
- mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority lost its original cloud receiver');
83
- }
92
+ originalReceiver(payload);
84
93
  if (delegated) preservedMutationDelegation(payload);
85
94
  }
86
95
  function authority(value, fields, schema, options) {
@@ -101,7 +110,16 @@ function authority(value, fields, schema, options) {
101
110
  identity(payload); mutationLifetime14(payload, options); return value;
102
111
  }
103
112
  export function mutationChallenge14(value, options) {
104
- return authority(value, authorityFields(value, CHALLENGE_FIELDS), 'entrorove.mutation-snapshot-challenge/1.0', options);
113
+ const fields = authorityFields(value, CHALLENGE_FIELDS);
114
+ if (Object.hasOwn(value ?? {}, 'previousChallenge')) {
115
+ exact(value.previousChallenge, ['challengeId', 'challengeDigest']);
116
+ if (!ID.test(value.previousChallenge.challengeId) || !SHA.test(value.previousChallenge.challengeDigest)
117
+ || value.verificationMode !== 'member-local' || !value.writeDelegation) {
118
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Expired challenge predecessor requires its exact member-local binding');
119
+ }
120
+ fields.push('previousChallenge');
121
+ }
122
+ return authority(value, fields, 'entrorove.mutation-snapshot-challenge/1.0', options);
105
123
  }
106
124
  function authorityFields(value, fields) {
107
125
  const original = Object.hasOwn(value ?? {}, 'writeDelegation') ? [...fields.filter(field =>
@@ -114,7 +132,8 @@ export function mutationPass14(value, options) {
114
132
  if (pass.disciplinePolicy) disciplineApprovedPolicy(pass.disciplinePolicy);
115
133
  if (pass.writeDelegation && (!pass.disciplinePolicy || pass.parallelPaths || pass.disciplinePolicy.dataOperations?.length
116
134
  || pass.paths.some(target => { const write = pass.disciplinePolicy.writes.find(item => item.targetPath === target);
117
- return !write || write.kind !== 'modify' || write.deletedSymbols?.length || write.exceptions?.length; }))) {
135
+ return !write || write.kind !== (pass.writeDelegation.purpose !== 'preserved-existing-modify' ? 'create' : 'modify')
136
+ || write.deletedSymbols?.length || write.exceptions?.length; }))) {
118
137
  mutationFail14('ENTROROVE_MUTATION_DELEGATION_INVALID', 'Preserved mutation permits only the exact existing modification policy');
119
138
  }
120
139
  if (pass.parallelPaths && !validatorParallelDeclarationsValid(pass.parallelPaths)) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Parallel paths must match the frozen equivalence contract');
@@ -25,7 +25,14 @@ export async function capturePreservedMutationProtection(input, root, authority,
25
25
  const delegation = preservedMutationDelegation(authority), review = await readDisciplineSnapshot(snapshot);
26
26
  for (const expected of delegation.targetFiles) {
27
27
  const file = snapshot.manifest.files.find(item => item.path === expected.path);
28
+ if (delegation.purpose === 'planned-file-create') {
29
+ if (!file || file.exists !== false || file.sha256 !== expected.sha256 || file.mode !== null) {
30
+ preservedMutationFail('Planned creation requires an actual absent target in the original M snapshot');
31
+ }
32
+ continue;
33
+ }
28
34
  if (!file?.exists || file.sha256 !== expected.sha256) preservedMutationFail('Preserved mutation target must retain its exact existing gate baseline');
35
+ if (delegation.purpose === 'planned-file-modify') continue;
29
36
  const imports = await scopeCandidateImports(root, { path: expected.path,
30
37
  content: (await scopeRead(root, expected.path, input)).content.toString('utf8') }, input);
31
38
  const declared = delegation.targetImports.find(item => item.path === expected.path);
@@ -35,7 +42,7 @@ export async function capturePreservedMutationProtection(input, root, authority,
35
42
  }
36
43
  }
37
44
  const unknown = review.analysis.unresolved.find(item => authority.paths.includes(item.path) && /(?:^import-|dynamic-import|dynamic-reference)/.test(item.reason));
38
- if (unknown) unknownTarget(unknown.path, unknown);
45
+ if (unknown && delegation.purpose === 'preserved-existing-modify') unknownTarget(unknown.path, unknown);
39
46
  const result = [];
40
47
  for (const expected of delegation.protectedFiles) result.push(await observe(input, root, expected));
41
48
  return result;
@@ -37,14 +37,18 @@ export function preservedMutationDelegation(authority) {
37
37
  || !local && authority.runnerKeyId === authority.authorityReceipt?.keyId) {
38
38
  preservedMutationFail('Preserved mutation cannot promote scope, mutation or workflow signing keys to independent validation');
39
39
  }
40
- if (value.schemaVersion !== 'entrorove.preserved-write-delegation/1.0' || value.purpose !== 'preserved-existing-modify'
40
+ const create = value.purpose === 'planned-file-create';
41
+ if (value.schemaVersion !== 'entrorove.preserved-write-delegation/1.0' || !['preserved-existing-modify', 'planned-file-create', 'planned-file-modify'].includes(value.purpose)
41
42
  || ['scopeKeyId', 'mutationKeyId', 'gateEvidenceDigest', 'importGraphDigest'].some(field => !/^[0-9a-f]{64}$/.test(value[field]))
42
43
  || publicKey.keyId !== value.mutationKeyId || new Set(roleKeys).size !== roleKeys.length
43
- || authority.epoch !== 0 || authority.newFilePaths.length !== 0) preservedMutationFail('Preserved mutation role or original epoch is invalid');
44
+ || authority.epoch !== 0 || (create ? canonical(authority.newFilePaths) !== canonical(authority.paths)
45
+ || value.targetFiles.some(file => file.sha256 !== sha256('entrorove.missing-file/1.0'))
46
+ : authority.newFilePaths.length !== 0)) preservedMutationFail('Preserved mutation role or original epoch is invalid');
44
47
  files(value.protectedFiles); files(value.targetFiles);
45
48
  if (canonical(value.targetFiles.map(file => file.path)) !== canonical(authority.paths)
46
49
  || value.protectedFiles.some(file => authority.paths.includes(file.path))
47
- || !Array.isArray(value.targetImports) || canonical(value.targetImports.map(file => file?.path)) !== canonical(authority.paths)) {
50
+ || !Array.isArray(value.targetImports) || (value.purpose !== 'preserved-existing-modify' ? value.targetImports.length !== 0
51
+ : canonical(value.targetImports.map(file => file?.path)) !== canonical(authority.paths))) {
48
52
  preservedMutationFail('Preserved mutation must retain its exact existing target and protected path sets');
49
53
  }
50
54
  for (const file of value.targetImports) {
@@ -2,6 +2,8 @@ import { constants, promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { canonical, sha256 } from './local-integrity.mjs';
4
4
  import { readBudgetedProjectFile } from './local-budget-reads.mjs';
5
+ import { verifiedSfnt } from './local-scope-font.mjs';
6
+ import { verifiedZip } from './local-scope-zip.mjs';
5
7
 
6
8
  export function scopeFail(code, message) { throw Object.assign(new Error(message), { code }); }
7
9
  export function observedScopeRisk(files) {
@@ -135,7 +137,7 @@ export async function scopeFiles(root, requested, knownFiles = []) {
135
137
  return [...found].sort();
136
138
  }
137
139
  export function lineCount(content) {
138
- if (!content.length) return 0;
140
+ if (!content.length || verifiedSfnt(content) || verifiedZip(content)) return 0;
139
141
  return content.toString('utf8').split(/\r\n|\n|\r/).length - (/[\r\n]$/.test(content.toString('utf8')) ? 1 : 0);
140
142
  }
141
143
  export async function packageBoundary(root, relative, input = { repositoryRoot: root }) {
@@ -0,0 +1,45 @@
1
+ function checksum(bytes, start, length, adjustment = -1) {
2
+ let sum = 0;
3
+ for (let offset = start; offset < start + length; offset += 4) {
4
+ let word = 0;
5
+ for (let i = 0; i < 4; i++) word = word * 256 + (offset + i < start + length ? bytes[offset + i] : 0);
6
+ if (offset !== adjustment) sum = (sum + word) >>> 0;
7
+ }
8
+ return sum;
9
+ }
10
+ // Recognize an sfnt container from its complete bytes, never its name. Unknown
11
+ // or damaged data keeps the conservative text count; a magic prefix is no proof.
12
+ export function verifiedSfnt(bytes) {
13
+ if (bytes.length < 28 || ![0x4f54544f, 0x00010000].includes(bytes.readUInt32BE(0))) return false;
14
+ const count = bytes.readUInt16BE(4), end = 12 + count * 16;
15
+ if (!count || count > 4096 || end > bytes.length) return false;
16
+ const selector = Math.floor(Math.log2(count)), search = 16 * 2 ** selector;
17
+ if (bytes.readUInt16BE(6) !== search || bytes.readUInt16BE(8) !== selector
18
+ || bytes.readUInt16BE(10) !== count * 16 - search) return false;
19
+ const tables = []; let previous = ''; let head;
20
+ for (let index = 12; index < end; index += 16) {
21
+ if (bytes.subarray(index, index + 4).some(byte => byte < 0x20 || byte > 0x7e)) return false;
22
+ const tag = bytes.toString('ascii', index, index + 4);
23
+ if (!/^[\x20-\x7e]{4}$/.test(tag) || tag <= previous) return false;
24
+ previous = tag;
25
+ const offset = bytes.readUInt32BE(index + 8), length = bytes.readUInt32BE(index + 12);
26
+ if (offset % 4 || offset < end || offset + length > bytes.length) return false;
27
+ const adjustment = tag === 'head' ? offset + 8 : -1;
28
+ if (tag === 'head') {
29
+ if (length !== 54 || bytes.readUInt32BE(offset) !== 0x00010000
30
+ || bytes.readUInt32BE(offset + 12) !== 0x5f0f3cf5) return false;
31
+ head = offset;
32
+ }
33
+ if (checksum(bytes, offset, length, adjustment) !== bytes.readUInt32BE(index + 4)) return false;
34
+ tables.push({ offset, length });
35
+ }
36
+ if (head === undefined || checksum(bytes, 0, bytes.length) !== 0xb1b0afba) return false;
37
+ let covered = end;
38
+ for (const table of tables.sort((a, b) => a.offset - b.offset)) {
39
+ if (table.offset < covered) return false;
40
+ for (; covered < table.offset; covered++) if (bytes[covered] !== 0) return false;
41
+ covered = table.offset + table.length;
42
+ }
43
+ for (; covered < bytes.length; covered++) if (bytes[covered] !== 0) return false;
44
+ return true;
45
+ }
@@ -264,17 +264,20 @@ async function configMetadata(root, relative, input) {
264
264
  async function resolveLocalImport(root, relative, required = true) {
265
265
  if (relative.startsWith('../') || relative === '..') scopeFail('ENTROROVE_PATH_UNSAFE', 'local import escapes the repository');
266
266
  const replacements = /\.js$/.test(relative) ? [relative.replace(/\.js$/, '.ts'), relative.replace(/\.js$/, '.tsx')] : [];
267
- for (const candidate of [relative, ...replacements, ...['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.d.ts', '.d.mts', '.d.cts', '/index.js', '/index.ts', '/index.d.ts'].map(ext => relative + ext)]) {
267
+ const candidates = [relative, ...replacements, ...['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.d.ts', '.d.mts', '.d.cts', '/index.js', '/index.ts', '/index.d.ts'].map(ext => relative + ext)];
268
+ for (const candidate of candidates) {
268
269
  const item = await scopeProbeTarget(root, candidate, dependencyReference(candidate));
269
270
  if (item.exists && item.stat.isFile()) return candidate;
270
271
  }
271
- if (required) scopeFail('ENTROROVE_IMPORT_UNRESOLVED', 'a local import has no resolvable source file');
272
+ if (required) throw Object.assign(new Error('a local import has no resolvable source file'),
273
+ { code: 'ENTROROVE_IMPORT_UNRESOLVED', missingCandidates: candidates });
272
274
  return null;
273
275
  }
274
276
  export async function importGraph(root, originals, input = { repositoryRoot: root }, limits = {}) {
275
277
  const pending = originals.filter(file => file.exists !== false).map(file => file.path); const visited = new Map(); const edges = []; let dynamic = false;
276
278
  let sourceBytes = 0;
277
279
  const dynamicSources = [], unresolvedImports = [];
280
+ const pendingCreates = new Set(originals.filter(file => file.exists === false && input.scopeCreatePaths?.includes(file.path)).map(file => file.path));
278
281
  const workspace = await workspaceIndex(root, input);
279
282
  const metadata = new Map(workspace.metadata.map(item => [item.path, item]));
280
283
  while (pending.length) {
@@ -300,7 +303,17 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
300
303
  unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
301
304
  if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
302
305
  for (const specifier of imports.specifiers.sort()) {
303
- const target = await resolveImport(root, relative, specifier, workspace, input, aliases, configPath, declarations);
306
+ let target;
307
+ try { target = await resolveImport(root, relative, specifier, workspace, input, aliases, configPath, declarations); }
308
+ catch (error) {
309
+ const missing = error.code === 'ENTROROVE_IMPORT_UNRESOLVED' && specifier.startsWith('.')
310
+ ? error.missingCandidates?.filter(candidate => pendingCreates.has(candidate)) : null;
311
+ if (!missing?.length) throw error;
312
+ unresolvedImports.push({ path: relative, specifier, reason: 'approved-create-not-present', candidatePaths: missing });
313
+ dynamic = true;
314
+ if (!dynamicSources.some(source => source.path === relative)) dynamicSources.push({ path: relative, sha256: read.sha256 });
315
+ continue;
316
+ }
304
317
  edges.push({ from: relative, to: target, external: target === null, specifier,
305
318
  ...(declarations.has(specifier) ? { declarationOnly: true, declarationPath: relative } : {}) });
306
319
  if (target !== null && !visited.has(target)) pending.push(target);
@@ -0,0 +1,93 @@
1
+ import { inflateRawSync } from 'node:zlib';
2
+
3
+ const MAX_ENTRY_BYTES = 16 * 1024 * 1024, MAX_TOTAL_BYTES = 64 * 1024 * 1024;
4
+ const crcTable = Uint32Array.from({ length: 256 }, (_, value) => {
5
+ for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
6
+ return value >>> 0;
7
+ });
8
+ function crc32(bytes) {
9
+ let value = 0xffffffff;
10
+ for (const byte of bytes) value = crcTable[(value ^ byte) & 255] ^ (value >>> 8);
11
+ return (value ^ 0xffffffff) >>> 0;
12
+ }
13
+ function plainExtra(bytes) {
14
+ let offset = 0;
15
+ while (offset + 4 <= bytes.length) {
16
+ const kind = bytes.readUInt16LE(offset), size = bytes.readUInt16LE(offset + 2);
17
+ if ([1, 0x9901].includes(kind) || offset + 4 + size > bytes.length) return false;
18
+ offset += 4 + size;
19
+ }
20
+ return offset === bytes.length;
21
+ }
22
+ function directory(bytes) {
23
+ const floor = Math.max(0, bytes.length - 65557); let end = bytes.length - 22;
24
+ for (; end >= floor; end--) {
25
+ if (bytes.readUInt32LE(end) === 0x06054b50 && end + 22 + bytes.readUInt16LE(end + 20) === bytes.length) break;
26
+ }
27
+ if (end < floor || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6)) return null;
28
+ const count = bytes.readUInt16LE(end + 10), offset = bytes.readUInt32LE(end + 16), size = bytes.readUInt32LE(end + 12);
29
+ if (count > 10_000 || count !== bytes.readUInt16LE(end + 8) || offset + size !== end) return null;
30
+ const entries = []; let cursor = offset, total = 0;
31
+ for (let index = 0; index < count; index++) {
32
+ if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) return null;
33
+ const nameLength = bytes.readUInt16LE(cursor + 28), extraLength = bytes.readUInt16LE(cursor + 30);
34
+ const next = cursor + 46 + nameLength + extraLength + bytes.readUInt16LE(cursor + 32);
35
+ const flags = bytes.readUInt16LE(cursor + 8), method = bytes.readUInt16LE(cursor + 10);
36
+ const compressed = bytes.readUInt32LE(cursor + 20), expanded = bytes.readUInt32LE(cursor + 24);
37
+ if (!nameLength || next > end || flags & ~0x080e || ![0, 8].includes(method) || bytes.readUInt16LE(cursor + 34)
38
+ || compressed === 0xffffffff || expanded > MAX_ENTRY_BYTES || (total += expanded) > MAX_TOTAL_BYTES
39
+ || !plainExtra(bytes.subarray(cursor + 46 + nameLength, cursor + 46 + nameLength + extraLength))) return null;
40
+ entries.push({ central: cursor, local: bytes.readUInt32LE(cursor + 42), flags, method, compressed, expanded,
41
+ crc: bytes.readUInt32LE(cursor + 16), name: bytes.subarray(cursor + 46, cursor + 46 + nameLength) });
42
+ cursor = next;
43
+ }
44
+ return cursor === end ? { entries, offset } : null;
45
+ }
46
+ function descriptorEnd(bytes, offset, entry, limit) {
47
+ for (const length of [16, 12]) {
48
+ const start = offset + length - 12;
49
+ if (offset + length > limit || length === 16 && bytes.readUInt32LE(offset) !== 0x08074b50) continue;
50
+ if (bytes.readUInt32LE(start) === entry.crc && bytes.readUInt32LE(start + 4) === entry.compressed
51
+ && bytes.readUInt32LE(start + 8) === entry.expanded) return offset + length;
52
+ }
53
+ return -1;
54
+ }
55
+ function entryEnd(bytes, entry, limit) {
56
+ const start = entry.local;
57
+ if (start + 30 > limit || bytes.readUInt32LE(start) !== 0x04034b50) return -1;
58
+ const nameLength = bytes.readUInt16LE(start + 26), extraLength = bytes.readUInt16LE(start + 28);
59
+ const data = start + 30 + nameLength + extraLength, end = data + entry.compressed;
60
+ if (end > limit || !bytes.subarray(start + 30, start + 30 + nameLength).equals(entry.name)
61
+ || !plainExtra(bytes.subarray(start + 30 + nameLength, data))) return -1;
62
+ for (const [local, central] of [[4, 6], [6, 8], [8, 10], [10, 12], [12, 14]]) {
63
+ if (bytes.readUInt16LE(start + local) !== bytes.readUInt16LE(entry.central + central)) return -1;
64
+ }
65
+ for (const [field, expected] of [[14, entry.crc], [18, entry.compressed], [22, entry.expanded]]) {
66
+ const actual = bytes.readUInt32LE(start + field);
67
+ if (actual !== expected && (!(entry.flags & 8) || actual !== 0)) return -1;
68
+ }
69
+ const compressed = bytes.subarray(data, end); let expanded;
70
+ if (entry.method === 0) expanded = compressed;
71
+ else {
72
+ try {
73
+ const result = inflateRawSync(compressed, { maxOutputLength: Math.max(1, entry.expanded), info: true });
74
+ if (result.engine.bytesWritten !== compressed.length) return -1;
75
+ expanded = result.buffer;
76
+ } catch { return -1; }
77
+ }
78
+ if (expanded.length !== entry.expanded || crc32(expanded) !== entry.crc) return -1;
79
+ return entry.flags & 8 ? descriptorEnd(bytes, end, entry, limit) : end;
80
+ }
81
+ // PKWARE APPNOTE 6.3.10, 4.3: verify complete ordinary ZIP records and CRCs
82
+ // in bounded memory. Never extract, execute entries or grant their paths access.
83
+ export function verifiedZip(bytes) {
84
+ if (bytes.length < 22 || ![0x04034b50, 0x06054b50].includes(bytes.readUInt32LE(0))) return false;
85
+ const found = directory(bytes); if (!found) return false;
86
+ let covered = 0;
87
+ for (const entry of found.entries.sort((a, b) => a.local - b.local)) {
88
+ if (entry.local !== covered) return false;
89
+ covered = entryEnd(bytes, entry, found.offset);
90
+ if (covered < 0) return false;
91
+ }
92
+ return covered === found.offset;
93
+ }
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.skill-manifest/1.0",
3
3
  "name": "entrorove",
4
- "version": "0.1.61",
4
+ "version": "0.1.64",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
@@ -0,0 +1,48 @@
1
+ import { canonical } from './local-integrity.mjs';
2
+ import { observationAuthoritySignature } from './workflow-observation-wire.mjs';
3
+
4
+ const keys = ['schemaVersion', 'repairId', 'flowId', 'ownerId', 'taskId', 'stepId', 'leaseId', 'generation', 'leaseDigest',
5
+ 'planId', 'planDigest', 'executionPlanSha256', 'failedObservationId', 'failureResultDigest', 'previousAcceptanceDigest',
6
+ 'kind', 'paths', 'dependencyRepairDigest', 'failureStepId', 'baselineSettlementId', 'baselineSettlementDigest',
7
+ 'grantId', 'sequence', 'previousGrantDigest', 'scopeDigest', 'requestId', 'issuedAt', 'expiresAt', 'authorityReceipt'];
8
+ const identity = ['flowId', 'ownerId', 'taskId', 'stepId', 'leaseId', 'generation', 'planId', 'planDigest', 'executionPlanSha256'];
9
+ const shas = ['leaseDigest', 'planDigest', 'executionPlanSha256', 'failureResultDigest', 'previousAcceptanceDigest', 'scopeDigest', 'requestId'];
10
+ export function memberProducerRepairStepMatches(status, authority, { settledPass = false } = {}) {
11
+ try {
12
+ const values = status?.producerRepairAuthorities;
13
+ if (!Array.isArray(values) || values.length > 128) return false;
14
+ const matching = values.filter(p => p.stepId === authority.stepId);
15
+ if (matching.length !== 1) return false;
16
+ const p = matching[0], expected = [...keys, ...(Object.hasOwn(p, 'scopeRevisionDigest') ? ['scopeRevisionDigest'] : [])];
17
+ if (canonical(Object.keys(p).sort()) !== canonical(expected.sort())) return false;
18
+ observationAuthoritySignature(p);
19
+ const start = Date.parse(p.issuedAt), end = Date.parse(p.expiresAt), at = Date.parse(authority.issuedAt);
20
+ if (p.schemaVersion !== 'entrorove.member-producer-repair/1.0' || identity.some(k => p[k] !== authority[k])
21
+ || status.flow?.stepId !== p.failureStepId || status.flow.ownerId !== p.ownerId
22
+ || status.flow.flowId !== p.flowId || status.flow.status !== 'running' || status.workflow?.phase !== 'execute'
23
+ || status.workflow.terminalAt || ['pending', 'declined'].includes(status.workflow.authorization)
24
+ || !['producer-correction', 'dependency-revalidation'].includes(p.kind)
25
+ || shas.some(k => !/^[a-f0-9]{64}$/.test(p[k])) || authority.leaseDigest && authority.leaseDigest !== p.leaseDigest
26
+ || !Array.isArray(p.paths) || p.paths.length > 512 || new Set(p.paths).size !== p.paths.length
27
+ || canonical(p.paths) !== canonical([...p.paths].sort()) || (p.kind === 'producer-correction') !== (p.paths.length > 0)
28
+ || p.paths.some(path => typeof path !== 'string' || !path || path.startsWith('/') || /[\\:\u0000-\u001f\u007f]/.test(path)
29
+ || path.split('/').some(part => !part || part === '.' || part === '..'))
30
+ || !Number.isSafeInteger(p.sequence) || p.sequence < 1 || !Number.isSafeInteger(p.generation) || p.generation < 1
31
+ || ['grantId', 'repairId', 'failedObservationId', 'failureStepId'].some(k => typeof p[k] !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(p[k]))
32
+ || (p.sequence === 1 ? p.previousGrantDigest !== null : !/^[a-f0-9]{64}$/.test(p.previousGrantDigest))
33
+ || !Number.isFinite(start) || !Number.isFinite(end) || start > Date.now() || end <= start || end - start > 600000
34
+ || !Number.isFinite(at) || at < start || at >= end || Date.parse(authority.expiresAt) > end
35
+ || !settledPass && end <= Date.now()
36
+ || (p.dependencyRepairDigest !== null && !/^[a-f0-9]{64}$/.test(p.dependencyRepairDigest))
37
+ || (Object.hasOwn(p, 'scopeRevisionDigest') && p.scopeRevisionDigest !== authority.scopeRevisionDigest
38
+ && Object.hasOwn(authority, 'scopeRevisionDigest'))) return false;
39
+ if (settledPass && (authority.verificationMode !== 'member-local' || authority.runnerKeyId !== null
40
+ || !authority.writeDelegation || p.kind !== 'producer-correction' || !Array.isArray(authority.paths)
41
+ || !authority.paths.length || authority.paths.some(path => !p.paths.includes(path)))) return false;
42
+ return true;
43
+ } catch { return false; }
44
+ }
45
+ export function memberProducerRepairDependency(status, pass) {
46
+ if (!memberProducerRepairStepMatches(status, pass, { settledPass: true })) return null;
47
+ return status.producerRepairAuthorities.find(p => p.stepId === pass.stepId).dependencyRepairDigest;
48
+ }
@@ -0,0 +1,9 @@
1
+ export type ModelImageInput = { url: string; detail?: 'auto'|'low'|'high'|'original' };
2
+ export type ModelImageMetadata = { sha256: string; mimeType: string; bytes: number; detail: 'auto'|'low'|'high'|'original' };
3
+ export type ModelImage = ModelImageInput & ModelImageMetadata;
4
+ export const MODEL_IMAGE_LIMITS: Readonly<{count: number; perImageBytes: number; totalBytes: number; requestBytes: number; maxSide: number}>;
5
+ export function inspectModelImage(bytes: Buffer): Omit<ModelImageMetadata,'detail'>;
6
+ export function normalizeModelImages(value?: unknown): ModelImage[];
7
+ export function modelImageManifest(images: readonly ModelImage[]): ModelImageMetadata[];
8
+ export function modelUserContent(goal: string, images: readonly ModelImage[]): string | Array<
9
+ { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string; detail: ModelImageMetadata['detail'] } }>;
@@ -0,0 +1,87 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const MODEL_IMAGE_LIMITS = Object.freeze({ count: 4, perImageBytes: 2 * 1024 * 1024,
4
+ totalBytes: 8 * 1024 * 1024, requestBytes: 12 * 1024 * 1024, maxSide: 8192 });
5
+ const DETAILS = new Set(['auto', 'low', 'high', 'original']);
6
+ const invalid = () => new Error('Images require bounded JPEG, PNG, GIF or WebP content with valid dimensions');
7
+
8
+ function jpegSize(bytes) {
9
+ let offset = 2;
10
+ while (offset + 4 <= bytes.length) {
11
+ if (bytes[offset++] !== 255) throw invalid();
12
+ while (bytes[offset] === 255) offset++;
13
+ if (offset + 3 > bytes.length) throw invalid();
14
+ const marker = bytes[offset++];
15
+ if (marker === 0xda || marker === 0xd9) break;
16
+ const length = bytes.readUInt16BE(offset);
17
+ if (length < 2 || offset + length > bytes.length) throw invalid();
18
+ if ([0xc0,0xc1,0xc2,0xc3,0xc5,0xc6,0xc7,0xc9,0xca,0xcb,0xcd,0xce,0xcf].includes(marker)) {
19
+ if (length < 8) throw invalid();
20
+ return [bytes.readUInt16BE(offset + 5), bytes.readUInt16BE(offset + 3)];
21
+ }
22
+ offset += length;
23
+ }
24
+ throw invalid();
25
+ }
26
+
27
+ function webpSize(bytes) {
28
+ if (bytes.length < 30 || bytes.readUInt32LE(4) + 8 !== bytes.length) throw invalid();
29
+ const chunk = bytes.toString('ascii', 12, 16), length = bytes.readUInt32LE(16);
30
+ if (20 + length + (length % 2) > bytes.length) throw invalid();
31
+ if (chunk === 'VP8X' && length === 10) return [bytes.readUIntLE(24, 3) + 1, bytes.readUIntLE(27, 3) + 1];
32
+ if (chunk === 'VP8 ' && length >= 10 && bytes.subarray(23,26).equals(Buffer.from([0x9d,1,0x2a]))) {
33
+ return [bytes.readUInt16LE(26) & 0x3fff, bytes.readUInt16LE(28) & 0x3fff];
34
+ }
35
+ if (chunk === 'VP8L' && length >= 5 && bytes[20] === 0x2f) {
36
+ const packed = bytes.readUInt32LE(21);
37
+ return [(packed & 0x3fff) + 1, ((packed >>> 14) & 0x3fff) + 1];
38
+ }
39
+ throw invalid();
40
+ }
41
+
42
+ /** Inspect content headers and dimensions; the provider still decodes the actual image. */
43
+ export function inspectModelImage(bytes) {
44
+ if (!Buffer.isBuffer(bytes) || bytes.length < 26 || bytes.length > MODEL_IMAGE_LIMITS.perImageBytes) throw invalid();
45
+ let mimeType, size;
46
+ if (bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))
47
+ && bytes.readUInt32BE(8) === 13 && bytes.toString('ascii',12,16) === 'IHDR') {
48
+ mimeType = 'image/png'; size = [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
49
+ } else if (bytes[0] === 255 && bytes[1] === 216) {
50
+ mimeType = 'image/jpeg'; size = jpegSize(bytes);
51
+ } else if (['GIF87a','GIF89a'].includes(bytes.toString('ascii',0,6))) {
52
+ mimeType = 'image/gif'; size = [bytes.readUInt16LE(6), bytes.readUInt16LE(8)];
53
+ } else if (bytes.toString('ascii',0,4) === 'RIFF' && bytes.toString('ascii',8,12) === 'WEBP') {
54
+ mimeType = 'image/webp'; size = webpSize(bytes);
55
+ } else throw invalid();
56
+ if (size.some(value => !Number.isInteger(value) || value < 1 || value > MODEL_IMAGE_LIMITS.maxSide)) throw invalid();
57
+ return { mimeType, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
58
+ }
59
+
60
+ export function normalizeModelImages(value = []) {
61
+ if (!Array.isArray(value) || value.length > MODEL_IMAGE_LIMITS.count) throw new Error('At most four input images are supported');
62
+ let total = 0;
63
+ return value.map(image => {
64
+ if (!image || typeof image !== 'object' || Array.isArray(image)
65
+ || Object.keys(image).some(key => !['url','detail'].includes(key))
66
+ || typeof image.url !== 'string' || !DETAILS.has(image.detail ?? 'auto')) throw invalid();
67
+ if (image.url.length > Math.ceil(MODEL_IMAGE_LIMITS.perImageBytes / 3) * 4 + 64) throw invalid();
68
+ const match = /^data:(image\/(?:jpeg|png|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.url);
69
+ if (!match) throw new Error('Input images must use inline base64 data URLs; remote URLs are not fetched');
70
+ const bytes = Buffer.from(match[2], 'base64');
71
+ if (bytes.toString('base64') !== match[2]) throw invalid();
72
+ const metadata = inspectModelImage(bytes);
73
+ if (metadata.mimeType !== match[1]) throw new Error('Image media type differs from its actual content');
74
+ total += bytes.length;
75
+ if (total > MODEL_IMAGE_LIMITS.totalBytes) throw new Error('Input images exceed the total byte limit');
76
+ return Object.freeze({ url: image.url, detail: image.detail ?? 'auto', ...metadata });
77
+ });
78
+ }
79
+
80
+ export function modelImageManifest(images) {
81
+ return images.map(({ sha256, mimeType, bytes, detail }) => ({ sha256, mimeType, bytes, detail }));
82
+ }
83
+
84
+ export function modelUserContent(goal, images) {
85
+ return images.length ? [{ type: 'text', text: goal }, ...images.map(image => ({ type: 'image_url',
86
+ image_url: { url: image.url, detail: image.detail } }))] : goal;
87
+ }
@@ -0,0 +1,51 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { buildInvocation } from './remote-client.mjs';
3
+ import { mutationFail14 } from './local-mutation-files.mjs';
4
+
5
+ const fail = message => mutationFail14('ENTROROVE_MUTATION_CHALLENGE_REFRESH', message);
6
+ export async function refreshExpiredMutationChallenge(context, callbacks) {
7
+ const { state, identity, mutator, location, dependencies, fields } = context;
8
+ const { remote, save, challengeBinding } = callbacks, old = state.challenged.challenge;
9
+ if (state.issued || state.gatePassPath || Date.parse(old.expiresAt) > Date.now()) return state.challenged;
10
+ challengeBinding(old, identity, mutator, { live: false });
11
+ if (old.verificationMode !== 'member-local' || !old.writeDelegation || old.runnerKeyId !== null) {
12
+ fail('Expired challenge recovery requires the original member-local editing authority');
13
+ }
14
+ if (!state.refreshOriginal && state.attestation) {
15
+ const input = { challengeId: old.challengeId, attestation: state.attestation };
16
+ if (!state.pending) {
17
+ state.pending = { stage: 'issued', operation: 'mutation-pass-issue', input,
18
+ requestId: buildInvocation('mutation-pass-issue', input).requestId };
19
+ await save(location, state);
20
+ }
21
+ if (state.pending.stage === 'issued') {
22
+ try { await remote('issued', 'mutation-pass-issue', input, state, location, dependencies); }
23
+ catch (error) {
24
+ if (state.pending || error.output?.status !== 'blocked'
25
+ || !['ENTROROVE_MUTATION_LEASE', 'ENTROROVE_MUTATION_EXPIRED'].includes(error.output.code)
26
+ || state.feedback?.acknowledgement?.stored !== true) throw error;
27
+ }
28
+ if (state.issued) return state.challenged;
29
+ }
30
+ }
31
+ if (state.pending && state.pending.stage !== 'refreshed') fail('Recover only the original pending mutation request before challenge refresh');
32
+ if (!Array.isArray(state.attempts ?? []) || (state.attempts?.length ?? 0) >= 512) fail('Original mutation challenge history reached its bound');
33
+ if (!state.refreshOriginal) {
34
+ state.refreshOriginal = Object.fromEntries(['challenged', 'planned', 'planStarted', 'attestation', 'feedback', 'failures']
35
+ .filter(key => Object.hasOwn(state, key)).map(key => [key, structuredClone(state[key])]));
36
+ await save(location, state);
37
+ }
38
+ if (canonical(state.refreshOriginal.challenged.challenge) !== canonical(old)) fail('Original archived challenge binding changed');
39
+ const ref = { challengeId: old.challengeId, challengeDigest: sha256(canonical(old)) };
40
+ const input = { ...Object.fromEntries(fields.map(field => [field, identity[field]])),
41
+ ...(identity.previousPassId ? { previousPassId: identity.previousPassId } : {}),
42
+ mutationPublicKey: mutator.mutationPublicKey, refreshExpiredChallenge: ref };
43
+ const refreshed = state.refreshed ?? await remote('refreshed', 'mutation-snapshot-challenge', input, state, location, dependencies);
44
+ challengeBinding(refreshed.challenge, identity, mutator, { live: false });
45
+ if (canonical(refreshed.challenge.previousChallenge) !== canonical(ref)) fail('Fresh challenge lost its exact expired predecessor');
46
+ state.attempts = [...(state.attempts ?? []), { ...state.refreshOriginal, archivedAt: new Date().toISOString() }];
47
+ for (const key of ['challenged', 'planned', 'planStarted', 'attestation', 'failures', 'refreshOriginal', 'refreshed']) delete state[key];
48
+ state.challenged = refreshed;
49
+ await save(location, state);
50
+ return refreshed;
51
+ }