feat(ql3): gate local secret transitions before activation

This commit is contained in:
whyour
2026-08-13 20:29:47 +08:00
parent 326783ff14
commit c877147d0f
37 changed files with 2495 additions and 39 deletions
@@ -286,6 +286,7 @@ test('recovers queued install through stage and activation without consuming app
retry: 0,
manualRequired: 0,
superseded: 0,
deferred: 0,
remaining: false,
safeToAdmit: true,
});
@@ -296,6 +297,62 @@ test('recovers queued install through stage and activation without consuming app
assert.deepEqual(calls, { stage: 1, publish: 1, inspect: 0 });
});
test('stages but defers activation until the exact prerequisite is ready', async () => {
const value = fixture();
const repository = new MemoryRepository([value]);
const calls = { stage: 0, publish: 0, inspect: 0, prerequisite: 0 };
let ready = false;
const coordinator = new PluginPackageRecoveryCoordinator({
repository,
stageProvider: {
async stage(lock) {
calls.stage += 1;
return stageEvidence(lock);
},
},
publisher: publisherFor(repository, calls),
activationPrerequisite: {
async inspect() {
calls.prerequisite += 1;
return ready
? { status: 'ready' }
: {
status: 'deferred',
reason: 'secret_binding_transition_required',
};
},
},
now: () => 250,
});
const deferred = await coordinator.recoverPage({ limit: 1 });
assert.equal(deferred.items[0].status, 'deferred');
assert.equal(
(await repository.find('default', 'example-monitor')).state,
'staged',
);
assert.deepEqual(calls, {
stage: 1,
publish: 0,
inspect: 0,
prerequisite: 2,
});
ready = true;
const settled = await coordinator.recoverPage({ limit: 1 });
assert.equal(settled.items[0].status, 'settled');
assert.equal(
(await repository.find('default', 'example-monitor')).state,
'active',
);
assert.deepEqual(calls, {
stage: 1,
publish: 1,
inspect: 0,
prerequisite: 3,
});
});
test('inspects an activating install without republishing it', async () => {
const value = activatingFixture();
const repository = new MemoryRepository([
@@ -0,0 +1,237 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createPluginPackageSecretBinding,
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
const {
createPluginPackageSecretBindingTransitionPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
const {
createPluginPackageSecretBindingFromTransitionPlan,
createPluginPackageSecretBindingTransitionReceipt,
normalizePluginPackageSecretBindingTransitionReceipt,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
function manifest(secrets) {
return {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: 'receipt-package',
displayName: 'Receipt package',
version: '2.0.0',
description: 'Transition receipt fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets,
tools: secrets.length === 0 ? [] : ['secret.use'],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
}
const previousManifest = manifest([{ name: 'TOKEN', required: true }]);
const previousGeneration = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'receipt-package',
lockDigest: 'a'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
contents: previousManifest.spec.contents,
});
const previousBinding = createPluginPackageSecretBinding({
generation: previousGeneration,
manifest: previousManifest,
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'token',
version: 1,
}),
},
],
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: 'c'.repeat(64),
},
boundAtMs: 10,
});
function transition(nextManifest, assignments) {
return createPluginPackageSecretBindingTransitionPlan({
previousTarget: previousBinding.target,
previousBinding,
previousAttemptGeneration: 1,
nextGeneration: createPluginPackageResourceGeneration({
installationId: 'install-2',
projectId: 'project-1',
packageName: 'receipt-package',
lockDigest: 'd'.repeat(64),
generation: 2,
previousActiveLockDigest: previousBinding.target.lockDigest,
contentDigest: 'e'.repeat(64),
contents: nextManifest.spec.contents,
}),
nextManifest,
assignments,
plannedAtMs: 20,
});
}
test('binds an exact next binding and approval evidence into one receipt', () => {
const plan = transition(previousManifest, [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'token',
version: 2,
}),
},
]);
const binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'approved-action-execution',
'f'.repeat(64),
30,
);
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'approved-action-execution',
evidenceDigest: 'f'.repeat(64),
},
binding,
committedAtMs: 30,
});
assert.equal(receipt.bindingDigest, binding.bindingDigest);
assert.equal(receipt.transitionPlan.kind, 'rotate');
assert.deepEqual(
normalizePluginPackageSecretBindingTransitionReceipt(receipt),
receipt,
);
});
test('records full revocation without inventing an empty binding', () => {
const plan = transition(manifest([]), []);
const binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'local-owner-confirmation',
plan.transitionDigest,
30,
);
assert.equal(binding, null);
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: plan.transitionDigest,
},
binding,
committedAtMs: 30,
});
assert.equal(receipt.bindingDigest, null);
assert.equal(receipt.transitionPlan.kind, 'revoke');
});
test('rejects missing, surplus, or authority-detached bindings', () => {
const plan = transition(previousManifest, [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'token',
version: 2,
}),
},
]);
assert.throws(() =>
createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: plan.transitionDigest,
},
binding: null,
committedAtMs: 30,
}),
);
const binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'local-owner-confirmation',
plan.transitionDigest,
30,
);
assert.throws(() =>
createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'approved-action-execution',
evidenceDigest: 'f'.repeat(64),
},
binding,
committedAtMs: 30,
}),
);
const revoke = transition(manifest([]), []);
assert.throws(() =>
createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: revoke,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: revoke.transitionDigest,
},
binding,
committedAtMs: 30,
}),
);
});
test('rejects receipt shape and digest tampering', () => {
const plan = transition(manifest([]), []);
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: plan.transitionDigest,
},
binding: null,
committedAtMs: 30,
});
assert.throws(() =>
normalizePluginPackageSecretBindingTransitionReceipt({
...receipt,
committedAtMs: 31,
}),
);
assert.throws(() =>
normalizePluginPackageSecretBindingTransitionReceipt({
...receipt,
unexpected: true,
}),
);
});