Skip to content

Commit 5de9041

Browse files
committed
Wire target_scope and parent_profile into exec handoff, document capability gating
- Fix prepareHandoff call in exec.js to pass target_scope and parent_profile so stripe-api-key provider receives the fields it requires for downscope - Document that child_credential_policy is intentionally not apply-time gated (runtime column accepted by all v23+ schedulers) - Add test for stripe-api-key handoff contract (target_scope + parent_profile required, well-formed handoff succeeds)
1 parent 2ca08b2 commit 5de9041

3 files changed

Lines changed: 49 additions & 4 deletions

File tree

src/capabilities.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ export function validateManifestCapabilities(compiledOutput, effectiveFeatures)
7777

7878
// Apply-time gating is intentionally limited to features that must exist to
7979
// persist or hand off the compiled durable job spec. Runtime identity
80-
// resolution and delegation validation remain execution-time concerns:
81-
// persisted identity declarations may already be sufficient for dispatch,
82-
// and delegation chains are only known after a concrete session is resolved.
80+
// resolution, delegation validation, and child_credential_policy remain
81+
// execution-time concerns: persisted identity declarations may already be
82+
// sufficient for dispatch, delegation chains are only known after a concrete
83+
// session is resolved, and child_credential_policy is a runtime column that
84+
// all v23+ schedulers accept regardless of whether providers are loaded.
8385
for (const job of compiledOutput.jobs) {
8486
// Check authorization hook requirement
8587
if (job.authorization || job.authorization_ref) {

src/exec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,11 @@ async function executeV2(common, {
10061006
try {
10071007
handoffResult = await identityProviderInstance.prepareHandoff(
10081008
identitySession,
1009-
{ mode: declaredHandoff },
1009+
{
1010+
mode: declaredHandoff,
1011+
target_scope: identityDeclaration.scope ?? identityDeclaration.auth?.scopes?.[0] ?? null,
1012+
parent_profile: identityDeclaration,
1013+
},
10101014
{ env, cwd }
10111015
);
10121016
} catch (err) {

test/agentcli.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9571,3 +9571,42 @@ test('stripe-api-key describeSession: masks key values (shows prefix + last 4 ch
95719571
assert.strictEqual(described.credentials.api_key.scope, 'payments');
95729572
assert.strictEqual(described.provider_assertions.account_mode, 'live');
95739573
});
9574+
9575+
test('stripe-api-key prepareHandoff: requires target_scope and parent_profile per provider contract', () => {
9576+
// Verify the provider rejects calls without target_scope/parent_profile
9577+
const parentSession = {
9578+
credentials: { api_key: { kind: 'bearer', value: 'sk_test_parent_full_key_123456', scope: 'full' } },
9579+
trust: { effective_level: 'supervised' },
9580+
};
9581+
const missingScope = stripeApiKeyProvider.prepareHandoff(parentSession, { parent_profile: {} }, {});
9582+
assert.strictEqual(missingScope.prepared, false);
9583+
assert.ok(missingScope.error.includes('target_scope'), 'error mentions target_scope');
9584+
9585+
const missingProfile = stripeApiKeyProvider.prepareHandoff(parentSession, { target_scope: 'readonly' }, {});
9586+
assert.strictEqual(missingProfile.prepared, false);
9587+
assert.ok(missingProfile.error.includes('parent_profile'), 'error mentions parent_profile');
9588+
9589+
// Verify a well-formed handoff with target_scope + parent_profile succeeds
9590+
const parentProfile = {
9591+
auth: {
9592+
provider_config: {
9593+
key_strategy: 'precreated',
9594+
account_mode: 'test',
9595+
permission_sets: {
9596+
full: { key_env: 'STRIPE_FULL' },
9597+
readonly: { key_env: 'STRIPE_READONLY' },
9598+
},
9599+
scope_hierarchy: { full: ['readonly'], readonly: [] },
9600+
},
9601+
},
9602+
};
9603+
const ctx = { env: { STRIPE_FULL: 'sk_test_full_key_abcdef123456', STRIPE_READONLY: 'rk_test_readonly_key_xyz789ab' } };
9604+
const result = stripeApiKeyProvider.prepareHandoff(
9605+
parentSession,
9606+
{ target_scope: 'readonly', parent_profile: parentProfile },
9607+
ctx,
9608+
);
9609+
assert.strictEqual(result.prepared, true);
9610+
assert.strictEqual(result.session.credentials.api_key.scope, 'readonly');
9611+
assert.strictEqual(result.session.credentials.api_key.value, 'rk_test_readonly_key_xyz789ab');
9612+
});

0 commit comments

Comments
 (0)