@entrorove/skill 0.1.62 → 0.1.67
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/CAPABILITY-MAP.md +30 -2
- package/CHANGELOG.md +25 -0
- package/DELIVERY-BOUNDARIES.md +2 -0
- package/DESIGN-GATES.md +160 -0
- package/IDE-EXECUTION.md +32 -3
- package/REUSE-DISCIPLINE.md +16 -2
- package/SKILL.md +9 -3
- package/blueprint-criterion-runner.mjs +1 -1
- package/cli-input.mjs +22 -0
- package/cli.mjs +13 -12
- package/cloud-task-client.mjs +10 -2
- package/cloud-task-images.mjs +39 -0
- package/design-browser-checks.mjs +86 -0
- package/design-browser-identity.mjs +13 -0
- package/design-browser-state.mjs +41 -0
- package/design-browser.mjs +181 -0
- package/design-context.mjs +34 -0
- package/design-contracts.d.mts +4 -0
- package/design-contracts.mjs +77 -0
- package/design-declaration.mjs +17 -0
- package/design-frameworks.mjs +4 -0
- package/design-gate.mjs +19 -0
- package/design-lint.mjs +115 -0
- package/design-local-contracts.mjs +15 -0
- package/design-operations.mjs +81 -0
- package/design-policy.d.mts +5 -0
- package/design-policy.mjs +137 -0
- package/design-release.mjs +25 -0
- package/design-research.mjs +99 -0
- package/design-runner.mjs +31 -0
- package/design-store.mjs +116 -0
- package/design-ui.mjs +24 -0
- package/ide-exit.mjs +49 -9
- package/ide-hook.mjs +2 -2
- package/install-identity.mjs +16 -0
- package/local-cli.mjs +3 -1
- package/local-contracts.mjs +6 -1
- package/local-discipline-actions.mjs +5 -0
- package/local-discipline-batch.mjs +12 -3
- package/local-discipline-contracts.mjs +15 -3
- package/local-discipline-gate.mjs +17 -2
- package/local-discipline-repository.mjs +11 -5
- package/local-error.mjs +18 -1
- package/local-execution.mjs +2 -1
- package/local-finder-plist.mjs +87 -0
- package/local-finder-store.mjs +115 -0
- package/local-mutation-authority.mjs +13 -0
- package/local-mutation-completion-evidence.mjs +83 -0
- package/local-mutation-consumption.mjs +33 -10
- package/local-mutation-environment-observation.mjs +85 -0
- package/local-mutation-finder-observation.mjs +58 -0
- package/local-mutation-wire.mjs +26 -8
- package/local-runner-discipline.mjs +17 -3
- package/local-scope-files.mjs +3 -1
- package/local-scope-font.mjs +45 -0
- package/local-scope-repository.mjs +16 -3
- package/local-scope-zip.mjs +114 -0
- package/manifest.json +1 -1
- package/member-producer-repair-authority.mjs +48 -0
- package/model-images.d.mts +9 -0
- package/model-images.mjs +87 -0
- package/mutation-challenge-refresh.mjs +51 -0
- package/mutation-client.mjs +42 -24
- package/mutation-connection-state.mjs +17 -0
- package/package-archive.mjs +54 -0
- package/package-replacement.mjs +48 -0
- package/package.json +50 -6
- package/release-state.json +87 -45
- package/release.mjs +11 -1
- package/releases.json +30 -0
- package/remote-client.mjs +1 -1
- package/remote-transport.mjs +48 -16
- package/reuse-discipline-release.mjs +3 -1
- package/rollback-history.mjs +51 -0
- package/rollback-runner.mjs +77 -0
- package/scope-amendment-client.mjs +1 -1
- package/scope-amendment-consumption.mjs +32 -0
- package/scope-amendment-mutation.mjs +2 -3
- package/scope-amendment-observer.mjs +2 -1
- package/scope-amendment-remote.mjs +12 -2
- package/scope-amendment-retirement.mjs +54 -0
- package/scope-amendment-state.mjs +7 -4
- package/task-entry-client.mjs +1 -0
- package/version.mjs +20 -3
- package/workflow-local-runner.mjs +9 -0
- package/workflow-observation-client.mjs +11 -0
- package/workflow-observation-connection.mjs +2 -1
- package/workflow-observation-remote.mjs +26 -1
- package/workflow-observation-wire.mjs +6 -3
package/remote-transport.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import https from 'node:https';
|
|
3
|
+
import { createGunzip } from 'node:zlib';
|
|
3
4
|
|
|
4
5
|
const PRE_CONNECT = new Set(['ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'ENETUNREACH',
|
|
5
6
|
'ENETDOWN', 'EHOSTUNREACH', 'ETIMEDOUT', 'ECONNABORTED']);
|
|
6
|
-
|
|
7
|
+
// A scope response retains its exact role map, signed S/W proof and recovery
|
|
8
|
+
// records together. Bound the whole envelope rather than one embedded record.
|
|
9
|
+
const MAX_BYTES = 8_388_608;
|
|
10
|
+
const RESPONSE_CODES = new Set(['RESPONSE_TOO_LARGE', 'RESPONSE_INTERRUPTED',
|
|
11
|
+
'RESPONSE_ENCODING_INVALID', 'RESPONSE_ENCODING_UNSUPPORTED']);
|
|
7
12
|
|
|
8
13
|
export function trustedOrigin(value) {
|
|
9
14
|
let url;
|
|
@@ -15,34 +20,60 @@ export function trustedOrigin(value) {
|
|
|
15
20
|
return url.origin;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function failure(code, established) {
|
|
19
|
-
return Object.assign(new Error(
|
|
23
|
+
function failure(code, established, detail = {}) {
|
|
24
|
+
return Object.assign(new Error(code === 'RESPONSE_TOO_LARGE' ? 'Entrorove response exceeds the bounded eight MiB envelope limit'
|
|
25
|
+
: code === 'RESPONSE_ENCODING_INVALID' ? 'Entrorove response has invalid compressed content'
|
|
26
|
+
: code === 'RESPONSE_ENCODING_UNSUPPORTED' ? 'Entrorove response uses an unnegotiated content encoding'
|
|
27
|
+
: established ? 'Entrorove response was interrupted after connection establishment'
|
|
20
28
|
: 'Entrorove connection failed before request transmission'), {
|
|
21
29
|
code: 'ENTROROVE_TRANSPORT', established, retryableBeforeTransmission: !established && PRE_CONNECT.has(code),
|
|
22
|
-
transportCode: PRE_CONNECT.has(code) ? code : 'CONNECTION_REJECTED',
|
|
30
|
+
transportCode: PRE_CONNECT.has(code) || RESPONSE_CODES.has(code) ? code : 'CONNECTION_REJECTED',
|
|
31
|
+
...(code === 'RESPONSE_TOO_LARGE' ? { responseLimitBytes: MAX_BYTES, receivedBytes: detail.receivedBytes } : {}),
|
|
23
32
|
});
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
function readResponse(response, request, done) {
|
|
36
|
+
const encoding = response.headers['content-encoding']?.trim().toLowerCase();
|
|
37
|
+
const decoded = encoding === 'gzip' ? createGunzip() : response;
|
|
38
|
+
const chunks = []; let size = 0; let wireBytes = 0; let ended = false;
|
|
39
|
+
const fail = cause => {
|
|
40
|
+
if (ended) return; ended = true; done(cause);
|
|
41
|
+
if (decoded !== response) decoded.destroy(); response.destroy(); request.destroy();
|
|
42
|
+
};
|
|
43
|
+
response.on('aborted', () => fail({ code: 'RESPONSE_INTERRUPTED' }));
|
|
44
|
+
response.on('error', cause => fail(cause));
|
|
45
|
+
if (encoding && !['identity', 'gzip'].includes(encoding)) {
|
|
46
|
+
fail({ code: 'RESPONSE_ENCODING_UNSUPPORTED' }); return;
|
|
47
|
+
}
|
|
48
|
+
if (decoded !== response) decoded.on('error', () => fail({ code: 'RESPONSE_ENCODING_INVALID' }));
|
|
49
|
+
response.on('data', chunk => {
|
|
50
|
+
if (ended) return; wireBytes += chunk.length;
|
|
51
|
+
if (wireBytes > MAX_BYTES) fail({ code: 'RESPONSE_TOO_LARGE', receivedBytes: wireBytes });
|
|
52
|
+
});
|
|
53
|
+
decoded.on('data', chunk => {
|
|
54
|
+
if (ended) return; size += chunk.length;
|
|
55
|
+
if (size > MAX_BYTES) fail({ code: 'RESPONSE_TOO_LARGE', receivedBytes: size });
|
|
56
|
+
else chunks.push(chunk);
|
|
57
|
+
});
|
|
58
|
+
decoded.on('end', () => {
|
|
59
|
+
if (ended) return; ended = true;
|
|
60
|
+
done(null, { ok: response.statusCode >= 200 && response.statusCode < 300,
|
|
61
|
+
status: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString('utf8') });
|
|
62
|
+
});
|
|
63
|
+
if (decoded !== response) response.pipe(decoded);
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
async function attempt(url, options) {
|
|
27
67
|
return new Promise((resolve, reject) => {
|
|
28
68
|
let established = false; let settled = false;
|
|
29
69
|
const done = (cause, result) => {
|
|
30
70
|
if (settled) return; settled = true; clearTimeout(timer);
|
|
31
|
-
if (cause) reject(failure(cause.code, established)); else resolve(result);
|
|
71
|
+
if (cause) reject(failure(cause.code, established, cause)); else resolve(result);
|
|
32
72
|
};
|
|
33
73
|
const client = url.protocol === 'https:' ? https : http;
|
|
34
74
|
const request = client.request(url, { method: options.method, headers: options.headers,
|
|
35
75
|
agent: false }, response => {
|
|
36
|
-
established = true;
|
|
37
|
-
response.on('data', chunk => {
|
|
38
|
-
size += chunk.length;
|
|
39
|
-
if (size > MAX_BYTES) { done({ code: 'RESPONSE_TOO_LARGE' }); response.destroy(); request.destroy(); }
|
|
40
|
-
else chunks.push(chunk);
|
|
41
|
-
});
|
|
42
|
-
response.on('aborted', () => done({ code: 'RESPONSE_INTERRUPTED' }));
|
|
43
|
-
response.on('error', cause => done(cause));
|
|
44
|
-
response.on('end', () => done(null, { ok: response.statusCode >= 200 && response.statusCode < 300,
|
|
45
|
-
status: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString('utf8') }));
|
|
76
|
+
established = true; readResponse(response, request, done);
|
|
46
77
|
});
|
|
47
78
|
request.on('socket', socket => {
|
|
48
79
|
socket.once(url.protocol === 'https:' ? 'secureConnect' : 'connect', () => { established = true; });
|
|
@@ -57,7 +88,8 @@ export async function requestTransport(origin, pathname, options = {}) {
|
|
|
57
88
|
const trusted = trustedOrigin(origin);
|
|
58
89
|
if (typeof pathname !== 'string' || !pathname.startsWith('/api/') || pathname.includes('://')) throw new Error('Invalid Entrorove API path');
|
|
59
90
|
const url = new URL(pathname, trusted);
|
|
60
|
-
const configured = { timeoutMs: 15_000, method: 'GET', ...options
|
|
91
|
+
const configured = { timeoutMs: 15_000, method: 'GET', ...options,
|
|
92
|
+
headers: { ...options.headers, 'accept-encoding': 'gzip' } };
|
|
61
93
|
const originalGeneration = configured.method === 'GET' && /^\/api\/skill\/feedback\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(url.pathname)
|
|
62
94
|
&& /^\?generation=[1-9][0-9]{0,14}$/.test(url.search) && Number.isSafeInteger(Number(url.searchParams.get('generation')));
|
|
63
95
|
if (url.origin !== trusted || url.hash || url.search && !originalGeneration) throw new Error('Invalid Entrorove API path');
|
|
@@ -8,13 +8,15 @@ const equivalence = ['local-equivalence-runner.mjs', 'validator-runtime-build.mj
|
|
|
8
8
|
'runtime/validator-runtime-v7.0.44.mjs', 'runtime/validator-runtime-v7.0.44.d.mts'];
|
|
9
9
|
const cleanup = ['snapshot-retention.mjs', 'snapshot-cleanup-journal.mjs'];
|
|
10
10
|
const documentation = ['IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md'];
|
|
11
|
-
|
|
11
|
+
const replacement = ['install-identity.mjs', 'package-archive.mjs', 'package-replacement.mjs', 'rollback-history.mjs', 'rollback-runner.mjs'];
|
|
12
|
+
export const REUSE_DISCIPLINE_SOURCE_FILES = [...ide, ...discipline, ...equivalence, ...cleanup, ...documentation, ...replacement, 'reuse-discipline-release.mjs'];
|
|
12
13
|
export const reuseDisciplineReleaseLabels = Object.fromEntries([
|
|
13
14
|
...ide.map(file => [file, { zh: 'Codex 后续轮次工具门禁', en: 'Codex tool guards across turns' }]),
|
|
14
15
|
...discipline.map(file => [file, { zh: '复用与变更一致性门禁', en: 'reuse and change consistency checks' }]),
|
|
15
16
|
...equivalence.map(file => [file, { zh: '真实新旧路径逐字段验证', en: 'actual fieldwise parallel-path verification' }]),
|
|
16
17
|
...cleanup.map(file => [file, { zh: '验收后快照自动清理', en: 'verified snapshot cleanup' }]),
|
|
17
18
|
...documentation.map(file => [file, { zh: 'IDE 执行与复用纪律文档', en: 'IDE execution and reuse discipline documentation' }]),
|
|
19
|
+
...replacement.map(file => [file, { zh: '包降级、版本冲突与凭证回滚门禁', en: 'package downgrade, version conflict and evidence-bound rollback guards' }]),
|
|
18
20
|
['reuse-discipline-release.mjs', { zh: '复用纪律版本追踪', en: 'reuse discipline release tracking' }],
|
|
19
21
|
['local-typescript-function-reference.mjs', { zh: 'TypeScript 类型与 Vue 属性构造器引用分析', en: 'TypeScript type and Vue prop constructor references' }],
|
|
20
22
|
['local-scope-import-expressions.mjs', { zh: '不可变有限动态导入与逐站点未核实诊断', en: 'immutable finite imports and precise unresolved import diagnostics' }],
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { atomicMutationJson14, mutationFail14, mutationFile14, mutationPath14, mutationStateRoot14,
|
|
4
|
+
privateMutationDirectory14, readMutationJson14 } from './local-mutation-files.mjs';
|
|
5
|
+
|
|
6
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
7
|
+
async function location(input, root, target, digest) {
|
|
8
|
+
mutationPath14(target);
|
|
9
|
+
if (!SHA.test(digest)) mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback comparison requires exact file digests');
|
|
10
|
+
const state = await mutationStateRoot14(input), repositoryDigest = sha256(root);
|
|
11
|
+
const directory = path.join(state, 'rollback-baselines', repositoryDigest, sha256(target));
|
|
12
|
+
return { state, directory, file: path.join(directory, `${digest}.json`), repositoryDigest };
|
|
13
|
+
}
|
|
14
|
+
async function read(loc, target, digest) {
|
|
15
|
+
let info;
|
|
16
|
+
try { info = await mutationFile14(loc.file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
17
|
+
if (info.stat.size > 8192) mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback baseline record exceeds its bounded schema');
|
|
18
|
+
const record = await readMutationJson14(loc.file), { recordDigest, ...body } = record;
|
|
19
|
+
if (body.schemaVersion !== 'entrorove.rollback-baseline/1.0' || body.repositoryDigest !== loc.repositoryDigest
|
|
20
|
+
|| body.targetPath !== target || body.baselineDigest !== digest || sha256(canonical(body)) !== recordDigest
|
|
21
|
+
|| !['mutation-snapshot', 'approved-process-comparison'].includes(body.proof?.kind)
|
|
22
|
+
|| !SHA.test(body.proof.authorityDigest ?? '') || !SHA.test(body.proof.baselineManifestDigest ?? '')
|
|
23
|
+
|| body.claimsCompletedWrite !== false || body.grantsRollbackAuthority !== false) {
|
|
24
|
+
mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback baseline record is invalid; preserve the record and original failure');
|
|
25
|
+
}
|
|
26
|
+
return record;
|
|
27
|
+
}
|
|
28
|
+
export async function assertRollbackCandidate(input, root, target, current, candidate) {
|
|
29
|
+
if (current === candidate) return;
|
|
30
|
+
const loc = await location(input, root, target, candidate);
|
|
31
|
+
if (await read(loc, target, candidate)) mutationFail14('ENTROROVE_ROLLBACK_PROOF_REQUIRED',
|
|
32
|
+
'Candidate restores an actually observed historical file image; use the original independently signed rollback with exact current/restore hashes and transaction records');
|
|
33
|
+
}
|
|
34
|
+
// These immutable observations prove only earlier bytes. They never claim a
|
|
35
|
+
// pending process finished, revive old write authority, or authorize rollback.
|
|
36
|
+
export async function rememberRollbackBaseline(input, root, target, digest, proof) {
|
|
37
|
+
const loc = await location(input, root, target, digest);
|
|
38
|
+
if (await read(loc, target, digest)) return;
|
|
39
|
+
const body = { schemaVersion: 'entrorove.rollback-baseline/1.0', repositoryDigest: loc.repositoryDigest,
|
|
40
|
+
targetPath: target, baselineDigest: digest, proof, claimsCompletedWrite: false, grantsRollbackAuthority: false };
|
|
41
|
+
await privateMutationDirectory14(loc.directory, { privateFrom: loc.state });
|
|
42
|
+
try { await atomicMutationJson14(loc.file, { ...body, recordDigest: sha256(canonical(body)) }, { privateFrom: loc.state }); }
|
|
43
|
+
catch (error) { if (error.code !== 'ENTROROVE_STATE_EXISTS' || !await read(loc, target, digest)) throw error; }
|
|
44
|
+
}
|
|
45
|
+
export async function rememberMutationBaselines(input, value, before) {
|
|
46
|
+
const proof = { kind: 'mutation-snapshot', authorityDigest: sha256(canonical(value.pass)),
|
|
47
|
+
baselineManifestDigest: value.pass.snapshotManifestDigest };
|
|
48
|
+
for (const file of before) if (file.exists) {
|
|
49
|
+
await rememberRollbackBaseline(input, value.binding.root, file.path, file.sha256, proof);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { mutationFail14, mutationRead14, mutationTarget14 } from './local-mutation-files.mjs';
|
|
4
|
+
import { archiveFormat } from './package-archive.mjs';
|
|
5
|
+
import { assertPackageReplacement } from './package-replacement.mjs';
|
|
6
|
+
import { assertRollbackCandidate, rememberRollbackBaseline } from './rollback-history.mjs';
|
|
7
|
+
|
|
8
|
+
const rollbackVerbs = new Set(['reset', 'restore', 'revert', 'checkout', 'switch']);
|
|
9
|
+
function gitOperation(args) {
|
|
10
|
+
for (let index = 0; index < args.length; index++) {
|
|
11
|
+
const value = args[index];
|
|
12
|
+
if (['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env'].includes(value)) { index++; continue; }
|
|
13
|
+
if (value.startsWith('-')) continue;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
export function assertNoBlindRollback(policy) {
|
|
19
|
+
const executable = path.basename(policy.executable).replace(/\.exe$/i, '').toLowerCase(), args = policy.args ?? [];
|
|
20
|
+
const direct = executable === 'git' && rollbackVerbs.has(gitOperation(args));
|
|
21
|
+
const shell = ['sh', 'bash', 'zsh', 'dash', 'cmd', 'powershell', 'pwsh'].includes(executable);
|
|
22
|
+
const script = args.join(' ');
|
|
23
|
+
const indirect = shell && /(?:^|[;&|\n\s])(?:\S*\/)?git\s+(?:(?:-C|-c|--git-dir|--work-tree)\s+\S+\s+)*(?:reset|restore|revert|checkout|switch)(?:\s|$)/i.test(script);
|
|
24
|
+
if (direct || indirect) mutationFail14('ENTROROVE_ROLLBACK_PROOF_REQUIRED',
|
|
25
|
+
'Direct rollback commands cannot run through an ordinary validation plan; use the independently signed exact-image rollback entry, preserving original transaction and failure records');
|
|
26
|
+
if (shell && /(?:^|[;&|\n\s])(?:\S*\/)?(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|update|upgrade|ci|exec|dlx)(?:\s|$)/i.test(script)) {
|
|
27
|
+
mutationFail14('ENTROROVE_PACKAGE_PROVENANCE_REQUIRED', 'Shell-wrapped package replacement cannot prove its destination or frozen inputs; use a direct frozen package-manager plan or the managed installer');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertFrozenInstall(authority) {
|
|
31
|
+
const policy = authority.plan.policy, executable = path.basename(policy.executable).replace(/\.(?:exe|cmd)$/i, '').toLowerCase();
|
|
32
|
+
const args = policy.args ?? [], operation = args.find(value => ['i', 'install', 'add', 'update', 'upgrade', 'ci', 'exec', 'dlx'].includes(value));
|
|
33
|
+
if (!['npm', 'pnpm', 'yarn', 'bun'].includes(executable) || !operation) return;
|
|
34
|
+
const install = ['install', 'i', 'ci'].includes(operation);
|
|
35
|
+
const frozen = executable === 'npm' ? operation === 'ci' : args.includes('--frozen-lockfile') || executable === 'yarn' && args.includes('--immutable');
|
|
36
|
+
const files = authority.plan.files ?? [], manifest = files.some(file => file.path === 'package.json');
|
|
37
|
+
const lock = files.some(file => (executable === 'npm' ? ['package-lock.json', 'npm-shrinkwrap.json']
|
|
38
|
+
: executable === 'pnpm' ? ['pnpm-lock.yaml'] : executable === 'yarn' ? ['yarn.lock'] : ['bun.lock', 'bun.lockb']).includes(file.path));
|
|
39
|
+
const positional = args.slice(args.indexOf(operation) + 1).filter((value, index, rest) => !value.startsWith('-') && !(index && rest[index - 1].startsWith('--')));
|
|
40
|
+
if (!install || !frozen || !manifest || !lock || positional.length || args.some(value => /^(?:--prefix|--dir|--cwd|--global|-g|--force)(?:=|$)/.test(value))) {
|
|
41
|
+
mutationFail14('ENTROROVE_PACKAGE_PROVENANCE_REQUIRED', 'Ordinary runner package installation requires its actual frozen manifest/lock in the original tested source set and no supplied package or destination override; use the managed installer for skill updates');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const packagePath = target => /(?:^|\/)(?:package|manifest)\.json$/i.test(target) || archiveFormat(target, Buffer.alloc(0));
|
|
45
|
+
export async function replacementRunnerBefore(input, authority) {
|
|
46
|
+
assertNoBlindRollback(authority.plan.policy);
|
|
47
|
+
assertFrozenInstall(authority);
|
|
48
|
+
const originals = new Map(), writes = authority.plan.policy.discipline?.writes ?? [];
|
|
49
|
+
for (const write of writes) {
|
|
50
|
+
const current = await mutationTarget14(authority.root, write.targetPath, true);
|
|
51
|
+
if (current.exists) {
|
|
52
|
+
if (current.stat.size > 64 * 1024 * 1024) {
|
|
53
|
+
if (packagePath(write.targetPath)) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Package baseline exceeds bounded inspection; no process was started');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const bytes = (await mutationRead14(input, authority.root, write.targetPath)).content;
|
|
57
|
+
if (packagePath(write.targetPath) || archiveFormat(write.targetPath, bytes)) originals.set(write.targetPath, bytes);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return originals;
|
|
61
|
+
}
|
|
62
|
+
export async function replacementRunnerAfter(input, authority, before, after, changed, originals) {
|
|
63
|
+
const proof = { kind: 'approved-process-comparison', authorityDigest: authority.planFile?.sha256 ?? sha256(canonical(authority.plan)),
|
|
64
|
+
baselineManifestDigest: sha256(canonical(before)) };
|
|
65
|
+
for (const target of changed) {
|
|
66
|
+
const previous = before.files.find(file => file.path === target), current = after.files.find(file => file.path === target);
|
|
67
|
+
if (!previous || !current || previous.kind !== 'file' || current.kind !== 'file') continue;
|
|
68
|
+
await assertRollbackCandidate(input, authority.root, target, previous.sha256, current.sha256);
|
|
69
|
+
if (previous.sha256 !== current.sha256 && (packagePath(target) || originals.has(target))) {
|
|
70
|
+
if (!originals.has(target)) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Process replaced an undeclared package without a verifiable original image; preserve this failed run and unknown effects');
|
|
71
|
+
const state = await mutationTarget14(authority.root, target);
|
|
72
|
+
if (state.stat.size > 64 * 1024 * 1024) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Replacement exceeds bounded package inspection');
|
|
73
|
+
assertPackageReplacement(target, originals.get(target), (await mutationRead14(input, authority.root, target)).content);
|
|
74
|
+
}
|
|
75
|
+
await rememberRollbackBaseline(input, authority.root, target, previous.sha256, proof);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -113,7 +113,7 @@ export async function runScopeAmend(repositoryRoot, input, dependencies) {
|
|
|
113
113
|
return { schemaVersion, status: 'blocked', flowId: input?.flowId,
|
|
114
114
|
code: error?.code?.startsWith('ENTROROVE_') ? error.code : 'ENTROROVE_SCOPE_AMENDMENT_PREREQUISITE',
|
|
115
115
|
message: ['ENTROROVE_SCOPE_AMENDMENT_BINDING', 'ENTROROVE_SCOPE_AMENDMENT_STATE', 'ENTROROVE_SCOPE_AMENDMENT_UNKNOWN',
|
|
116
|
-
'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT'].includes(error?.code) ? error.message
|
|
116
|
+
'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT', 'ENTROROVE_SCOPE_AMENDMENT_MUTATION_PENDING'].includes(error?.code) ? error.message
|
|
117
117
|
: 'Scope amendment stopped; preserve the original proposal, signer, journal and actual evidence.',
|
|
118
118
|
...(requestId ? { requestId } : {}), ...(error?.output ? { output: error.output } : {}),
|
|
119
119
|
...(error?.feedback ? { feedback: error.feedback } : {}),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
4
|
+
import { mutationFail14, privateMutationDirectory14 } from './local-mutation-files.mjs';
|
|
5
|
+
import { preservedMutationConsumptionState, attestMutationConsumption14 } from './local-mutation-consumption.mjs';
|
|
6
|
+
import { readMutationConnection } from './mutation-connection-state.mjs';
|
|
7
|
+
|
|
8
|
+
export async function observeScopeMutationConsumption(location, state, pass) {
|
|
9
|
+
// A pending S keeps its exact old attestation and is recovered with GET. A
|
|
10
|
+
// new signed S challenge can observe the same physical writes afresh, but
|
|
11
|
+
// must not fork an unresolved settlement of that original M receipt.
|
|
12
|
+
if (state.pending || state.attestation) mutationFail14('ENTROROVE_SCOPE_AMENDMENT_MUTATION_PENDING',
|
|
13
|
+
'Recover the original pending scope request before observing its M consumption again');
|
|
14
|
+
const input = { repositoryRoot: location.root, home: location.home,
|
|
15
|
+
flowId: pass.flowId, taskId: pass.taskId, stepId: pass.stepId, gatePassPath: location.identity.gatePassPath };
|
|
16
|
+
const original = await preservedMutationConsumptionState(input), { binding } = original;
|
|
17
|
+
if (canonical(original.pass) !== canonical(pass)) mutationFail14('ENTROROVE_SCOPE_AMENDMENT_BINDING', 'Original mutation pass changed');
|
|
18
|
+
const directory = path.join(binding.directory, 'settlements');
|
|
19
|
+
await privateMutationDirectory14(directory, { privateFrom: binding.stateRoot });
|
|
20
|
+
const journal = { ...binding, file: path.join(directory, `${pass.passId}.json`) };
|
|
21
|
+
const identity = { flowId: pass.flowId, stepId: pass.stepId, passDigest: sha256(canonical(pass)),
|
|
22
|
+
repositoryRoot: binding.root, apiOrigin: location.identity.apiOrigin, mutationKeyId: binding.binding.keyId };
|
|
23
|
+
return withStateLock(journal.file, async () => {
|
|
24
|
+
const settlement = await readMutationConnection(journal, identity);
|
|
25
|
+
if (settlement.pending || settlement.settled) mutationFail14('ENTROROVE_SCOPE_AMENDMENT_MUTATION_PENDING',
|
|
26
|
+
'Recover the original mutation-settle request before a fresh scope observation; do not replace its receipt');
|
|
27
|
+
// Renewal re-reads every original target and protected file, verifies the
|
|
28
|
+
// old signature and write times, and archives the expired observation.
|
|
29
|
+
// The original pass, write ledger and their expiry remain unchanged.
|
|
30
|
+
return attestMutationConsumption14(input, { renewExpired: true });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mutationPass14 } from './local-mutation-wire.mjs';
|
|
2
2
|
import { readMutationJson14 } from './local-mutation-files.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { observeScopeMutationConsumption } from './scope-amendment-consumption.mjs';
|
|
4
4
|
import { amendmentFail } from './scope-amendment-wire.mjs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { sha256 } from './local-integrity.mjs';
|
|
@@ -20,7 +20,6 @@ export async function scopeAmendmentMutation(location, state, challenge, facts)
|
|
|
20
20
|
|| pass.memberScopeKeyId !== challenge.keyId || pass.memberScopeBindingDigest !== challenge.originalBindingDigest
|
|
21
21
|
|| pass.scopeRevisionDigest !== challenge.authorityRevisionDigest
|
|
22
22
|
|| changed.some(file => !pass.paths.includes(file.path))) amendmentFail('Scope refresh cannot replace its actual original mutation pass or approved write paths');
|
|
23
|
-
const observed = await
|
|
24
|
-
flowId: pass.flowId, taskId: pass.taskId, stepId: pass.stepId, gatePassPath }, { allowExpiredReplay: true });
|
|
23
|
+
const observed = await observeScopeMutationConsumption(location, state, pass);
|
|
25
24
|
return { passId: pass.passId, consumptionReceipt: observed.consumptionReceipt };
|
|
26
25
|
}
|
|
@@ -27,7 +27,8 @@ export async function observeScopeAmendment(location, state) {
|
|
|
27
27
|
if (!state.snapshotId) { state.snapshotId = `scope-amend-${randomUUID()}`; await saveScopeAmendmentState(location, state); }
|
|
28
28
|
const input = { repositoryRoot: location.root, home: location.home, flowId: challenge.flowId, stepId: challenge.stepId,
|
|
29
29
|
snapshotId: state.snapshotId, paths: challenge.files.map(file => file.path).sort(),
|
|
30
|
-
contractDigest: challenge.proposalDigest, importContexts: location.binding.importContexts
|
|
30
|
+
contractDigest: challenge.proposalDigest, importContexts: location.binding.importContexts,
|
|
31
|
+
scopeCreatePaths: challenge.files.filter(file => file.access === 'create').map(file => file.path) };
|
|
31
32
|
if (!state.snapshot) {
|
|
32
33
|
if (!state.snapshotStarted) {
|
|
33
34
|
state.snapshotStarted = true; await saveScopeAmendmentState(location, state);
|
|
@@ -6,6 +6,7 @@ import { acknowledgeAutomaticFeedback, queryAutomaticFeedback, verifyAutomaticFe
|
|
|
6
6
|
import { readInvocations } from './local-invocations.mjs';
|
|
7
7
|
import { saveScopeAmendmentState } from './scope-amendment-state.mjs';
|
|
8
8
|
import { amendmentFail } from './scope-amendment-wire.mjs';
|
|
9
|
+
import { retireMissingChallenge } from './scope-amendment-retirement.mjs';
|
|
9
10
|
|
|
10
11
|
const unknown = requestId => Object.assign(new Error('Scope amendment outcome is unknown; recover the original request with GET only'),
|
|
11
12
|
{ code: 'ENTROROVE_SCOPE_AMENDMENT_UNKNOWN', outcomeUnknown: true, requestId });
|
|
@@ -15,13 +16,16 @@ async function recover(pending, dependencies) {
|
|
|
15
16
|
const response = await requestTransport(origin, `/api/skill/status/${pending.requestId}`, {
|
|
16
17
|
headers: { authorization: `Bearer ${token}`, ...ideClientHeaders() } });
|
|
17
18
|
const result = parseTransportResponse(response, 'original scope amendment status');
|
|
19
|
+
if (response.status === 404 && result?.error === 'request-not-found') {
|
|
20
|
+
throw Object.assign(unknown(pending.requestId), { requestNotFound: true });
|
|
21
|
+
}
|
|
18
22
|
if (result?.output?.requestId !== pending.requestId || response.status >= 500
|
|
19
23
|
|| ['running', 'pending'].includes(result.output.status)) throw unknown(pending.requestId);
|
|
20
24
|
result.feedback ??= await queryAutomaticFeedback(pending.requestId, dependencies);
|
|
21
25
|
verifyAutomaticFeedback(result.feedback, pending.requestId, pending.requestId);
|
|
22
26
|
result.feedback = await acknowledgeAutomaticFeedback(result.feedback, origin, token, ideClientHeaders());
|
|
23
27
|
return { response, result };
|
|
24
|
-
} catch { throw unknown(pending.requestId); }
|
|
28
|
+
} catch (error) { throw error?.requestNotFound ? error : unknown(pending.requestId); }
|
|
25
29
|
}
|
|
26
30
|
async function notSent(pending, home) {
|
|
27
31
|
try {
|
|
@@ -36,7 +40,13 @@ export async function amendmentRemote(stage, operation, payload, state, location
|
|
|
36
40
|
if (state.pending.stage !== stage || state.pending.requestId !== requestId || canonical(state.pending.input) !== canonical(input)) {
|
|
37
41
|
amendmentFail('Recover the original amendment request before another operation');
|
|
38
42
|
}
|
|
39
|
-
(
|
|
43
|
+
if (state.challengeRetirement) await retireMissingChallenge(state, location, dependencies, recover, notSent);
|
|
44
|
+
try { ({ response, result } = await recover(state.pending, dependencies)); }
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!error?.requestNotFound || stage !== 'challenged') throw error;
|
|
47
|
+
await retireMissingChallenge(state, location, dependencies, recover, notSent);
|
|
48
|
+
({ response, result } = await recover(state.pending, dependencies));
|
|
49
|
+
}
|
|
40
50
|
} else {
|
|
41
51
|
state.pending = { stage, operation: 'continue', input, requestId };
|
|
42
52
|
await saveScopeAmendmentState(location, state);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
2
|
+
import { buildInvocation, invokeRemoteResult } from './remote-client.mjs';
|
|
3
|
+
import { scopeRevisionAuthority, amendmentFail } from './scope-amendment-wire.mjs';
|
|
4
|
+
import { saveScopeAmendmentState } from './scope-amendment-state.mjs';
|
|
5
|
+
|
|
6
|
+
export async function retireMissingChallenge(state, location, dependencies, recover, notSent) {
|
|
7
|
+
const original = state.pending;
|
|
8
|
+
if (original.stage !== 'challenged' || original.input.operation !== 'workflow-scope-challenge') {
|
|
9
|
+
amendmentFail('Only the original missing challenge preparation has a retirement path');
|
|
10
|
+
}
|
|
11
|
+
const input = { requestId: original.requestId, flowId: original.input.flowId, payload: original.input.payload };
|
|
12
|
+
const call = buildInvocation('scope-challenge-retire', input);
|
|
13
|
+
let attempt = state.challengeRetirement;
|
|
14
|
+
if (attempt && (attempt.requestId !== call.requestId || canonical(attempt.input) !== canonical(input))) {
|
|
15
|
+
amendmentFail('Challenge retirement must retain its original preparation envelope');
|
|
16
|
+
}
|
|
17
|
+
if (!attempt) {
|
|
18
|
+
attempt = state.challengeRetirement = { requestId: call.requestId, input, state: 'pending' };
|
|
19
|
+
await saveScopeAmendmentState(location, state);
|
|
20
|
+
try {
|
|
21
|
+
const { result } = await invokeRemoteResult({ operation: 'scope-challenge-retire', input }, dependencies);
|
|
22
|
+
attempt.output = result.output; attempt.feedback = result.feedback; attempt.state = 'completed';
|
|
23
|
+
await saveScopeAmendmentState(location, state);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (await notSent(attempt, dependencies.home)) {
|
|
26
|
+
attempt.state = 'not-sent'; await saveScopeAmendmentState(location, state);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
} else if (attempt.state !== 'completed') {
|
|
31
|
+
// A retirement with an uncertain outcome has its own original GET. Neither
|
|
32
|
+
// the original challenge nor the retirement is retransmitted here.
|
|
33
|
+
if (attempt.state === 'not-sent') {
|
|
34
|
+
delete state.challengeRetirement; await saveScopeAmendmentState(location, state);
|
|
35
|
+
return retireMissingChallenge(state, location, dependencies, recover, notSent);
|
|
36
|
+
}
|
|
37
|
+
const { result } = await recover(attempt, dependencies);
|
|
38
|
+
attempt.output = result.output; attempt.feedback = result.feedback; attempt.state = 'completed';
|
|
39
|
+
await saveScopeAmendmentState(location, state);
|
|
40
|
+
}
|
|
41
|
+
const output = attempt.output;
|
|
42
|
+
if (output?.requestId !== call.requestId) amendmentFail('Retirement lost its original request identity');
|
|
43
|
+
if (output.status === 'blocked' && output.result?.reason === 'original-admission-already-recorded') return;
|
|
44
|
+
if (output.status !== 'succeeded' || output.result?.retired !== true) amendmentFail('The server did not retire this original missing preparation');
|
|
45
|
+
const receipt = scopeRevisionAuthority(output.result.retirement, 'entrorove.scope-challenge-retirement/1.0');
|
|
46
|
+
if (receipt.purpose !== 'scope-challenge-retirement' || receipt.requestId !== original.requestId
|
|
47
|
+
|| receipt.retirementRequestId !== call.requestId || receipt.flowId !== input.flowId
|
|
48
|
+
|| receipt.proposalId !== input.payload.proposalId || receipt.proposalDigest !== input.payload.proposalDigest
|
|
49
|
+
|| receipt.receiverId !== input.payload.receiverId || receipt.attemptId !== input.payload.attemptId
|
|
50
|
+
|| receipt.originalInputDigest !== sha256(canonical(original.input)) || receipt.outcome !== 'retired-before-admission'
|
|
51
|
+
|| ['claimsOriginalExecution', 'grantsExecutionAuthority', 'grantsWriteAuthority', 'claimsCompletion'].some(key => receipt[key] !== false)) {
|
|
52
|
+
amendmentFail('Challenge retirement changed its exact original preparation or authority boundary');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -10,11 +10,14 @@ import { trustedOrigin } from './remote-transport.mjs';
|
|
|
10
10
|
|
|
11
11
|
const fail = message => scopeFail('ENTROROVE_SCOPE_AMENDMENT_STATE', message);
|
|
12
12
|
const maximumStateBytes = 1_000_000;
|
|
13
|
-
|
|
13
|
+
// A journal retains the challenge, S proof, original pending request and full
|
|
14
|
+
// signed response together. Each signed authority retains its own 1 MB bound.
|
|
15
|
+
const maximumJournalBytes = 8_000_000;
|
|
16
|
+
async function privateItem(file, final = 'file', maximumBytes = maximumStateBytes) {
|
|
14
17
|
const value = await mutationFile14(file, { final });
|
|
15
18
|
if (process.getuid && value.stat.uid !== process.getuid()
|
|
16
19
|
|| process.platform !== 'win32' && value.stat.mode & 0o077
|
|
17
|
-
|| final === 'file' && (value.stat.nlink !== 1 || value.stat.size >
|
|
20
|
+
|| final === 'file' && (value.stat.nlink !== 1 || value.stat.size > maximumBytes)) {
|
|
18
21
|
fail('Scope amendment controls must be bounded private files owned by the current OS account');
|
|
19
22
|
}
|
|
20
23
|
return value;
|
|
@@ -80,7 +83,7 @@ export async function scopeAmendmentLocation(repositoryRoot, input, dependencies
|
|
|
80
83
|
}
|
|
81
84
|
export async function readScopeAmendmentState(location) {
|
|
82
85
|
let value;
|
|
83
|
-
try { await privateItem(location.file); value = await readMutationJson14(location.file); }
|
|
86
|
+
try { await privateItem(location.file, 'file', maximumJournalBytes); value = await readMutationJson14(location.file); }
|
|
84
87
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
85
88
|
if (!value) return { schemaVersion: 'entrorove.scope-amendment-state/1.0', identity: location.identity };
|
|
86
89
|
if (value.schemaVersion !== 'entrorove.scope-amendment-state/1.0'
|
|
@@ -89,6 +92,6 @@ export async function readScopeAmendmentState(location) {
|
|
|
89
92
|
return value;
|
|
90
93
|
}
|
|
91
94
|
export async function saveScopeAmendmentState(location, value) {
|
|
92
|
-
if (Buffer.byteLength(canonical(value)) >
|
|
95
|
+
if (Buffer.byteLength(canonical(value)) + 1 > maximumJournalBytes) fail('Scope amendment state exceeds its bounded private file limit');
|
|
93
96
|
return atomicMutationJson14(location.file, value, { privateFrom: location.stateRoot, replace: true });
|
|
94
97
|
}
|
package/task-entry-client.mjs
CHANGED
|
@@ -28,5 +28,6 @@ export async function routeTaskEntry(data, token, dependencies) {
|
|
|
28
28
|
if (!task || typeof task.goal !== 'string') throw new Error('Task entry requires the original task goal');
|
|
29
29
|
const ideTaskId = ideTaskIdentity(data.ideTaskId);
|
|
30
30
|
return runCloudTask({ goal: task.goal, task,
|
|
31
|
+
...(data.images === undefined ? {} : { images: data.images }),
|
|
31
32
|
...(ideTaskId ? { ideTaskId } : {}),...(choice ? { sandboxChoice: choice } : {}) },dependencies);
|
|
32
33
|
}
|
package/version.mjs
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
// SemVer 2 precedence, without Number rounding or lexical version ordering.
|
|
2
|
+
export function versionParts(value) {
|
|
3
|
+
const match = typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
|
|
4
|
+
if (!match || match[4]?.split('.').some(part => /^\d+$/.test(part) && part.length > 1 && part[0] === '0')) {
|
|
5
|
+
throw Object.assign(new Error('Package version must be an unambiguous SemVer value'), { code: 'ENTROROVE_PACKAGE_VERSION_UNKNOWN' });
|
|
6
|
+
}
|
|
7
|
+
return { core: match.slice(1, 4).map(BigInt), pre: match[4]?.split('.') ?? [] };
|
|
8
|
+
}
|
|
1
9
|
export function compareVersions(left, right) {
|
|
2
|
-
const a = left
|
|
3
|
-
const b = right.split('.').map(Number);
|
|
10
|
+
const a = versionParts(left), b = versionParts(right);
|
|
4
11
|
for (let index = 0; index < 3; index += 1) {
|
|
5
|
-
if (a[index] !== b[index]) return a[index]
|
|
12
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1;
|
|
13
|
+
}
|
|
14
|
+
if (!a.pre.length || !b.pre.length) return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1;
|
|
15
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
|
|
16
|
+
const x = a.pre[index], y = b.pre[index];
|
|
17
|
+
if (x === y) continue;
|
|
18
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
19
|
+
const numericX = /^\d+$/.test(x), numericY = /^\d+$/.test(y);
|
|
20
|
+
if (numericX && numericY) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
21
|
+
if (numericX !== numericY) return numericX ? -1 : 1;
|
|
22
|
+
return x > y ? 1 : -1;
|
|
6
23
|
}
|
|
7
24
|
return 0;
|
|
8
25
|
}
|
|
@@ -312,6 +312,15 @@ export async function observeWorkflowLocalStep(rawInput) {
|
|
|
312
312
|
return { ...structuredClone(state.result), cached: true };
|
|
313
313
|
}));
|
|
314
314
|
}
|
|
315
|
+
export function verifyOriginalWorkflowPlan(value) {
|
|
316
|
+
const configured = publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY');
|
|
317
|
+
const plan = validatePlan(verifyAuthority(value, configured));
|
|
318
|
+
lifetime(plan.acceptedAt, plan.expiresAt, MAX_PLAN_MS, Date.now());
|
|
319
|
+
return plan;
|
|
320
|
+
}
|
|
321
|
+
export function verifyOriginalWorkflowAuthority(value) {
|
|
322
|
+
return verifyAuthority(value, publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY'));
|
|
323
|
+
}
|
|
315
324
|
export async function runWorkflowLocalStep(rawInput) {
|
|
316
325
|
let input; try { input = structuredClone(rawInput); } catch { fail('ENTROROVE_WORKFLOW_JSON', 'workflow values must be finite JSON'); }
|
|
317
326
|
const authority = await prepare(input);
|
|
@@ -5,6 +5,7 @@ const safeId = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-
|
|
|
5
5
|
const safeRequest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
6
6
|
const messages = {
|
|
7
7
|
ENTROROVE_LOCAL_OBSERVATION_UNKNOWN: 'Original observation outcome is unknown; recover its original GET without repeating execution',
|
|
8
|
+
ENTROROVE_LOCAL_OBSERVATION_REJECTED: 'Original observation request failed and was acknowledged; repair its cause and read the original live task guidance',
|
|
8
9
|
ENTROROVE_LOCAL_OBSERVATION_NOT_SENT: 'Observation was not transmitted; repair its local or discovery prerequisite and reuse the original input',
|
|
9
10
|
ENTROROVE_LOCAL_OBSERVATION_PREREQUISITE: 'Local observation requires cloud sandbox disabled and its original live execution lease',
|
|
10
11
|
ENTROROVE_SCOPE_RECEIPT_EXPIRED: 'Refresh the original scope evidence and reuse the original observation input',
|
|
@@ -24,6 +25,11 @@ function blocked(error, input) {
|
|
|
24
25
|
'ENTROROVE_SCOPE_RUNNER_REQUIRED', 'ENTROROVE_SCOPE_SNAPSHOT_REQUIRED', 'ENTROROVE_SCOPE_AFTER_WRITE_REQUIRED'].includes(code));
|
|
25
26
|
const reason = ['sandbox-enabled', 'cloud-sandbox-enabled', 'lease-expired', 'receipt-expired', 'after-write-observation-required'].includes(output?.reason)
|
|
26
27
|
? output.reason : undefined;
|
|
28
|
+
const rejected = code === 'ENTROROVE_LOCAL_OBSERVATION_REJECTED' && cause.knownTerminalFailure === true
|
|
29
|
+
&& requestId && cause.rejection?.status === 'failed' && cause.rejection.acknowledged === true
|
|
30
|
+
&& /^[a-f0-9-]{36}$/.test(cause.rejection.receiptId ?? '')
|
|
31
|
+
&& Number.isSafeInteger(cause.rejection.generation) && cause.rejection.generation > 0
|
|
32
|
+
&& ['member-observation-admit', 'member-observation-report'].includes(cause.rejection.operation);
|
|
27
33
|
return { schemaVersion: 'entrorove.local-observation-connection/1.0', status: 'blocked',
|
|
28
34
|
...(safeId(input?.flowId) ? { flowId: input.flowId } : {}), independentValidation: 'pending', code,
|
|
29
35
|
message: messages[code] ?? 'Local observation could not continue; its original command and result remain preserved',
|
|
@@ -33,6 +39,11 @@ function blocked(error, input) {
|
|
|
33
39
|
...(safeRequest(output.requestId) ? { resumeRequestId: output.requestId } : {}) } } } : {}),
|
|
34
40
|
...(cause.outcomeUnknown === true && requestId ? { outcomeUnknown: true, requestId, repeatPost: false,
|
|
35
41
|
nextAction: { operation: 'workflow-observe-step', reuseOriginalInput: true, method: 'GET' } } : {}),
|
|
42
|
+
...(rejected ? { outcomeUnknown: false, requestId, repeatPost: false, grantsExecutionAuthority: false,
|
|
43
|
+
rejection: { operation: cause.rejection.operation, status: 'failed', receiptId: cause.rejection.receiptId,
|
|
44
|
+
generation: cause.rejection.generation, acknowledged: true, effects: 'not-inferred' },
|
|
45
|
+
nextAction: { operation: 'task-resume', input: { flowId: input.flowId }, preserveOriginalFlow: true,
|
|
46
|
+
localProcessMayStart: false, originalRequestRetained: true } } : {}),
|
|
36
47
|
...(cause.knownBeforeTransmission === true && requestId ? { requestId, knownBeforeTransmission: true } : {}) };
|
|
37
48
|
}
|
|
38
49
|
export async function runWorkflowObserveStep(repositoryRoot, input, dependencies) {
|
|
@@ -9,7 +9,8 @@ import { verifyWorkflowLocalAcceptance } from './workflow-local-acceptance.mjs';
|
|
|
9
9
|
import { buildInvocation } from './remote-client.mjs';
|
|
10
10
|
|
|
11
11
|
async function connect(input, state, location, dependencies) {
|
|
12
|
-
const fields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation', ...(Object.hasOwn(input, 'settlementId') ? ['settlementId'] : [])
|
|
12
|
+
const fields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation', ...(Object.hasOwn(input, 'settlementId') ? ['settlementId'] : []),
|
|
13
|
+
...(Object.hasOwn(input, 'dependencyRepairDigest') ? ['dependencyRepairDigest'] : [])];
|
|
13
14
|
const original = Object.fromEntries(fields.map(key => [key, input[key]]));
|
|
14
15
|
const admitted = state.admitted ?? await observationRemote('admitted', 'member-observation-admit', original, state, location, dependencies);
|
|
15
16
|
const admission = observationAdmission(admitted.admission, input, { allowExpired: Boolean(state.result) });
|
|
@@ -51,6 +51,18 @@ function cachedReportPrerequisite(result, pending, state, location) {
|
|
|
51
51
|
&& next.reuseOriginalInput === true && next.resumeRequestId === pending.requestId && next.preserveOriginalFlow === true
|
|
52
52
|
&& result.feedback?.acknowledgement?.stored === true && result.feedback.resultDigest === sha256(canonical(output));
|
|
53
53
|
}
|
|
54
|
+
function terminalRejection(result, pending) {
|
|
55
|
+
const output = result?.output, feedback = result?.feedback, ack = feedback?.acknowledgement;
|
|
56
|
+
try { verifyAutomaticFeedback(feedback, pending.requestId, pending.requestId); } catch { return null; }
|
|
57
|
+
if (output?.schemaVersion !== 'entrorove.skill.response/1.0' || output.requestId !== pending.requestId
|
|
58
|
+
|| output.status !== 'failed' || feedback.resultDigest !== sha256(canonical(output))
|
|
59
|
+
|| feedback.evaluation.operation !== pending.operation || feedback.evaluation.status !== 'failed'
|
|
60
|
+
|| ack?.stored !== true || ack.requestId !== pending.requestId || ack.receiptId !== feedback.receiptId
|
|
61
|
+
|| ack.generation !== (feedback.generation ?? 1)) return null;
|
|
62
|
+
return { operation: pending.operation, status: 'failed', receiptId: feedback.receiptId,
|
|
63
|
+
generation: feedback.generation ?? 1, acknowledged: true, effects: 'not-inferred',
|
|
64
|
+
...(/^[a-z][a-z0-9-]{0,63}$/.test(output.error ?? '') ? { error: output.error } : {}) };
|
|
65
|
+
}
|
|
54
66
|
export async function observationRemote(stage, operation, input, state, location, dependencies) {
|
|
55
67
|
const requestId = buildInvocation(operation, input).requestId;
|
|
56
68
|
let response, result;
|
|
@@ -79,6 +91,17 @@ export async function observationRemote(stage, operation, input, state, location
|
|
|
79
91
|
throw Object.assign(new Error(result.output.message ?? 'Original local observation prerequisite is unresolved'),
|
|
80
92
|
{ code: result.output.code, output: result.output });
|
|
81
93
|
}
|
|
94
|
+
const rejection = terminalRejection(result, state.pending);
|
|
95
|
+
if (rejection) {
|
|
96
|
+
const failure = { ...state.pending, output: result.output, feedback: result.feedback };
|
|
97
|
+
if (canonical(state.failures?.at(-1) ?? null) !== canonical(failure)) state.failures = [...(state.failures ?? []), failure];
|
|
98
|
+
state.rejection = failure;
|
|
99
|
+
// Keep the original pending binding: a known failure grants no retry,
|
|
100
|
+
// no replacement admission and no second launch of a cached process.
|
|
101
|
+
await saveObservationState(location, state);
|
|
102
|
+
throw Object.assign(new Error('The original observation request failed and its receipt was acknowledged'),
|
|
103
|
+
{ code: 'ENTROROVE_LOCAL_OBSERVATION_REJECTED', knownTerminalFailure: true, requestId, rejection });
|
|
104
|
+
}
|
|
82
105
|
throw unknown(requestId);
|
|
83
106
|
}
|
|
84
107
|
state[stage] = result.output; delete state.pending; await saveObservationState(location, state);
|
|
@@ -91,9 +114,11 @@ export async function authorizeObservation(admission, dependencies, launching) {
|
|
|
91
114
|
const status = parseTransportResponse(response, 'original local observation account');
|
|
92
115
|
if (!response.ok || status?.schemaVersion !== 'entrorove.flow-status/1.0'
|
|
93
116
|
|| status.flow?.flowId !== admission.flowId || status.flow.ownerId !== admission.ownerId
|
|
94
|
-
|| launching && (status.flow.stepId !== admission.stepId
|
|
117
|
+
|| launching && (status.flow.stepId !== admission.stepId && !memberProducerRepairStepMatches(status, admission)
|
|
118
|
+
|| status.flow.status !== 'running'
|
|
95
119
|
|| status.workflow?.phase !== 'execute' || status.workflow.terminalAt
|
|
96
120
|
|| ['pending', 'declined'].includes(status.workflow.authorization))) {
|
|
97
121
|
observationFail('Current account cannot confirm its original local observation task and execution step');
|
|
98
122
|
}
|
|
99
123
|
}
|
|
124
|
+
import { memberProducerRepairStepMatches } from './member-producer-repair-authority.mjs';
|