gemstack-ai 1.2.0 → 1.4.0

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 (37) hide show
  1. package/.gemstack/state.json +11 -10
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +89 -9
  4. package/RELEASE_NOTES.md +77 -0
  5. package/{gemstack-ai-1.2.0.tgz → gemstack-ai-1.4.0.tgz} +0 -0
  6. package/handoff.md +14 -12
  7. package/package.json +2 -2
  8. package/specs/008-cost-provider-safety-gates/.gemstack.json +5 -0
  9. package/specs/008-cost-provider-safety-gates/closure.json +59 -0
  10. package/specs/008-cost-provider-safety-gates/plan.md +456 -0
  11. package/specs/008-cost-provider-safety-gates/spec.md +633 -0
  12. package/specs/008-cost-provider-safety-gates/tasks.md +635 -0
  13. package/specs/009-context-capsule/closure.json +59 -0
  14. package/specs/009-context-capsule/context-capsule.json +428 -0
  15. package/specs/009-context-capsule/plan.md +663 -0
  16. package/specs/009-context-capsule/spec.md +913 -0
  17. package/specs/009-context-capsule/tasks.md +720 -0
  18. package/specs/010-agent-swarm-visual-qa/.gemstack.json +5 -0
  19. package/specs/010-agent-swarm-visual-qa/closure.json +59 -0
  20. package/specs/010-agent-swarm-visual-qa/plan.md +759 -0
  21. package/specs/010-agent-swarm-visual-qa/spec.md +842 -0
  22. package/specs/010-agent-swarm-visual-qa/swarm.json +49 -0
  23. package/specs/010-agent-swarm-visual-qa/tasks.md +873 -0
  24. package/specs/010-agent-swarm-visual-qa/visual-qa.json +41 -0
  25. package/src/cli.js +10 -0
  26. package/src/commands/context.js +95 -0
  27. package/src/commands/swarm.js +111 -0
  28. package/src/commands/verify.js +92 -0
  29. package/src/commands/visual.js +82 -0
  30. package/src/lib/closure-context.js +18 -1
  31. package/src/lib/context-capsule.js +594 -0
  32. package/src/lib/cost-ledger.js +355 -0
  33. package/src/lib/provider-boundary.js +186 -0
  34. package/src/lib/provider-registry.js +265 -0
  35. package/src/lib/safety-gates.js +277 -0
  36. package/src/lib/swarm.js +639 -0
  37. package/src/lib/visual-qa.js +499 -0
@@ -0,0 +1,265 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { createFinding } = require('./findings');
4
+
5
+ const VALID_ENV_TIERS = ['test', 'ci', 'development', 'staging', 'production'];
6
+ const PROVIDER_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
7
+ const REMOTE_URL_PATTERN = /^https?:\/\//i;
8
+
9
+ /**
10
+ * Resolves the deterministic environment tier.
11
+ *
12
+ * @param {object} [options={}]
13
+ * @param {string} [options.environment]
14
+ * @param {object} [options.env=process.env]
15
+ * @returns {string} One of: 'test', 'ci', 'development', 'staging', 'production'
16
+ */
17
+ function resolveEnvironmentTier(options = {}) {
18
+ const env = options.env || process.env;
19
+
20
+ if (options.environment && VALID_ENV_TIERS.includes(options.environment)) {
21
+ return options.environment;
22
+ }
23
+
24
+ if (env.GEMSTACK_ENV && VALID_ENV_TIERS.includes(env.GEMSTACK_ENV)) {
25
+ return env.GEMSTACK_ENV;
26
+ }
27
+
28
+ // Detect CI first
29
+ if (env.CI === 'true' || env.CI === '1' || env.CONTINUOUS_INTEGRATION === 'true') {
30
+ return 'ci';
31
+ }
32
+
33
+ // Detect test runner
34
+ if (env.NODE_ENV === 'test' || typeof global.it === 'function' || typeof global.test === 'function' || process.argv.includes('--test')) {
35
+ return 'test';
36
+ }
37
+
38
+ return 'development';
39
+ }
40
+
41
+ /**
42
+ * Validates whether commercial providers are permitted in a given environment tier.
43
+ *
44
+ * @param {string} envTier
45
+ * @param {object} [options={}]
46
+ * @param {boolean} [options.allowBillable=false]
47
+ * @param {object} [options.ciWaiver=null]
48
+ * @returns {{ allowed: boolean, reasonCode: string, message: string }}
49
+ */
50
+ function checkEnvironmentCommercialPolicy(envTier, options = {}) {
51
+ if (envTier === 'test') {
52
+ return {
53
+ allowed: false,
54
+ reasonCode: 'ENV_COMMERCIAL_DENIED',
55
+ message: 'Commercial provider invocation is strictly denied in test environment.'
56
+ };
57
+ }
58
+
59
+ if (envTier === 'ci') {
60
+ if (options.ciWaiver && options.ciWaiver.signed === true && options.ciWaiver.expires_at > Date.now()) {
61
+ return {
62
+ allowed: true,
63
+ reasonCode: 'CI_WAIVER_ACCEPTED',
64
+ message: 'Commercial invocation permitted in CI via explicit signed waiver.'
65
+ };
66
+ }
67
+ return {
68
+ allowed: false,
69
+ reasonCode: 'ENV_COMMERCIAL_DENIED',
70
+ message: 'Commercial provider invocation is denied in CI environment without explicit signed waiver.'
71
+ };
72
+ }
73
+
74
+ if (envTier === 'development') {
75
+ if (!options.allowBillable) {
76
+ return {
77
+ allowed: false,
78
+ reasonCode: 'BILLABLE_ACTION_UNAUTHORIZED',
79
+ message: 'Commercial provider actions in development require explicit authorization (--allow-billable).'
80
+ };
81
+ }
82
+ return {
83
+ allowed: true,
84
+ reasonCode: 'DEV_AUTHORIZED',
85
+ message: 'Commercial execution authorized for development session.'
86
+ };
87
+ }
88
+
89
+ // staging / production
90
+ return {
91
+ allowed: true,
92
+ reasonCode: 'PRODUCTION_ALLOWED',
93
+ message: 'Commercial execution permitted under managed environment.'
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Validates integrity of a MOCK provider configuration to prevent network escape.
99
+ *
100
+ * @param {object} providerConfig
101
+ * @returns {{ valid: boolean, reasonCode: string|null, message: string|null }}
102
+ */
103
+ function validateMockIntegrity(providerConfig) {
104
+ if (!providerConfig || typeof providerConfig !== 'object') {
105
+ return {
106
+ valid: false,
107
+ reasonCode: 'MOCK_PROVIDER_ESCAPE_VIOLATION',
108
+ message: 'Mock provider configuration must be a valid object.'
109
+ };
110
+ }
111
+
112
+ if (providerConfig.type !== 'MOCK') {
113
+ return {
114
+ valid: false,
115
+ reasonCode: 'MOCK_PROVIDER_ESCAPE_VIOLATION',
116
+ message: 'Provider is not declared with type "MOCK".'
117
+ };
118
+ }
119
+
120
+ // Scan for remote endpoints, urls, or external host configurations
121
+ const forbiddenFields = ['url', 'endpoint', 'baseUrl', 'base_url', 'host', 'remoteUrl', 'remote_url'];
122
+ for (const field of forbiddenFields) {
123
+ if (typeof providerConfig[field] === 'string' && providerConfig[field].trim()) {
124
+ const val = providerConfig[field].trim();
125
+ if (REMOTE_URL_PATTERN.test(val) || val.includes('://') || (!val.includes('localhost') && !val.includes('127.0.0.1') && val.includes('.'))) {
126
+ return {
127
+ valid: false,
128
+ reasonCode: 'MOCK_PROVIDER_ESCAPE_VIOLATION',
129
+ message: 'Mock provider declared forbidden remote endpoint at field "' + field + '": ' + val
130
+ };
131
+ }
132
+ }
133
+ }
134
+
135
+ // Scan nested capabilities for remote endpoint overrides
136
+ if (providerConfig.capabilities && typeof providerConfig.capabilities === 'object') {
137
+ for (const [capId, cap] of Object.entries(providerConfig.capabilities)) {
138
+ if (cap && typeof cap === 'object') {
139
+ for (const field of forbiddenFields) {
140
+ if (typeof cap[field] === 'string' && cap[field].trim()) {
141
+ return {
142
+ valid: false,
143
+ reasonCode: 'MOCK_PROVIDER_ESCAPE_VIOLATION',
144
+ message: 'Mock capability "' + capId + '" declared forbidden remote endpoint at "' + field + '".'
145
+ };
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ return {
153
+ valid: true,
154
+ reasonCode: null,
155
+ message: null
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Creates an in-memory ProviderRegistry instance.
161
+ *
162
+ * @param {object} providersMap
163
+ * @returns {object} ProviderRegistry
164
+ */
165
+ function createProviderRegistry(providersMap = {}) {
166
+ const normalizedProviders = new Map();
167
+
168
+ for (const [rawId, config] of Object.entries(providersMap)) {
169
+ const id = rawId.toLowerCase().trim();
170
+ if (!PROVIDER_ID_REGEX.test(id)) {
171
+ throw new Error('Invalid provider ID format: "' + rawId + '". Must match slug pattern: ' + PROVIDER_ID_REGEX.toString());
172
+ }
173
+
174
+ const type = (config.type || 'COMMERCIAL').toUpperCase();
175
+ const capabilities = new Map();
176
+
177
+ if (config.capabilities) {
178
+ if (Array.isArray(config.capabilities)) {
179
+ for (const cap of config.capabilities) {
180
+ capabilities.set(cap, { id: cap });
181
+ }
182
+ } else if (typeof config.capabilities === 'object') {
183
+ for (const [capId, capVal] of Object.entries(config.capabilities)) {
184
+ capabilities.set(capId, capVal);
185
+ }
186
+ }
187
+ }
188
+
189
+ normalizedProviders.set(id, {
190
+ ...config,
191
+ id,
192
+ type,
193
+ capabilities
194
+ });
195
+ }
196
+
197
+ return {
198
+ hasProvider(providerId) {
199
+ if (typeof providerId !== 'string') return false;
200
+ return normalizedProviders.has(providerId.toLowerCase().trim());
201
+ },
202
+
203
+ getProvider(providerId) {
204
+ if (typeof providerId !== 'string') return null;
205
+ return normalizedProviders.get(providerId.toLowerCase().trim()) || null;
206
+ },
207
+
208
+ hasCapability(providerId, capabilityId) {
209
+ const p = this.getProvider(providerId);
210
+ if (!p) return false;
211
+ return p.capabilities.has(capabilityId);
212
+ },
213
+
214
+ getCapability(providerId, capabilityId) {
215
+ const p = this.getProvider(providerId);
216
+ if (!p) return null;
217
+ return p.capabilities.get(capabilityId) || null;
218
+ },
219
+
220
+ listProviders() {
221
+ return Array.from(normalizedProviders.values());
222
+ }
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Loads provider registry from repository artifacts or ledger.
228
+ *
229
+ * @param {string} rootPath
230
+ * @param {string} [featureDir='']
231
+ * @returns {object} ProviderRegistry
232
+ */
233
+ function loadProviderRegistry(rootPath, featureDir = '') {
234
+ const candidatePaths = [
235
+ path.join(rootPath, featureDir, 'cost-ledger.json'),
236
+ path.join(rootPath, 'cost-ledger.json'),
237
+ path.join(rootPath, '.gemstack/cost-ledger.json'),
238
+ path.join(rootPath, '.gemstack/providers.json')
239
+ ];
240
+
241
+ for (const p of candidatePaths) {
242
+ if (fs.existsSync(p)) {
243
+ try {
244
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
245
+ if (raw.providers && typeof raw.providers === 'object') {
246
+ return createProviderRegistry(raw.providers);
247
+ }
248
+ } catch (e) {
249
+ // Fall through to next candidate
250
+ }
251
+ }
252
+ }
253
+
254
+ return createProviderRegistry({});
255
+ }
256
+
257
+ module.exports = {
258
+ VALID_ENV_TIERS,
259
+ PROVIDER_ID_REGEX,
260
+ resolveEnvironmentTier,
261
+ checkEnvironmentCommercialPolicy,
262
+ validateMockIntegrity,
263
+ createProviderRegistry,
264
+ loadProviderRegistry
265
+ };
@@ -0,0 +1,277 @@
1
+ const { checkEnvironmentCommercialPolicy, resolveEnvironmentTier } = require('./provider-registry');
2
+
3
+ /**
4
+ * Creates a normalized, structured gate decision object.
5
+ *
6
+ * @param {object} params
7
+ * @param {boolean} params.authorized
8
+ * @param {string} params.decision - 'ALLOW' | 'DENY'
9
+ * @param {string} params.reasonCode
10
+ * @param {string} params.message
11
+ * @param {object} [params.context={}]
12
+ * @returns {object} Structured gate decision
13
+ */
14
+ function createGateDecision({ authorized, decision, reasonCode, message, context = {} }) {
15
+ // Strip any accidental credential fields from context
16
+ const cleanContext = {};
17
+ for (const [k, v] of Object.entries(context)) {
18
+ const lk = k.toLowerCase();
19
+ if (!lk.includes('key') && !lk.includes('secret') && !lk.includes('token') && !lk.includes('pass')) {
20
+ cleanContext[k] = v;
21
+ }
22
+ }
23
+
24
+ return {
25
+ authorized: Boolean(authorized),
26
+ decision: decision || (authorized ? 'ALLOW' : 'DENY'),
27
+ reasonCode,
28
+ reason_code: reasonCode, // backward/spec compatibility
29
+ message,
30
+ context: cleanContext
31
+ };
32
+ }
33
+
34
+ /**
35
+ * ProviderCapabilityGate: Validates provider registration, environment allowance,
36
+ * capability declaration, and adapter execution support.
37
+ *
38
+ * @param {object} request
39
+ * @param {string} request.provider_id
40
+ * @param {string} request.capability_id
41
+ * @param {string} [request.environment]
42
+ * @param {object} [request.adapter]
43
+ * @param {object} registry - ProviderRegistry instance
44
+ * @param {object} [options={}]
45
+ * @returns {object} Structured gate decision
46
+ */
47
+ function evaluateProviderCapability(request, registry, options = {}) {
48
+ if (!request || typeof request !== 'object') {
49
+ return createGateDecision({
50
+ authorized: false,
51
+ decision: 'DENY',
52
+ reasonCode: 'UNKNOWN_PROVIDER_IDENTITY',
53
+ message: 'Capability evaluation request must be an object.',
54
+ context: {}
55
+ });
56
+ }
57
+
58
+ const providerId = (request.provider_id || '').toLowerCase().trim();
59
+ const capabilityId = request.capability_id;
60
+ const envTier = resolveEnvironmentTier({ environment: request.environment, ...options });
61
+
62
+ // 1. Is Provider Registered?
63
+ if (!providerId || !registry || !registry.hasProvider(providerId)) {
64
+ return createGateDecision({
65
+ authorized: false,
66
+ decision: 'DENY',
67
+ reasonCode: 'UNKNOWN_PROVIDER_IDENTITY',
68
+ message: 'Provider "' + (request.provider_id || '') + '" is not registered in provider registry.',
69
+ context: { provider_id: request.provider_id, capability_id: capabilityId, environment: envTier }
70
+ });
71
+ }
72
+
73
+ const provider = registry.getProvider(providerId);
74
+
75
+ // 2. Is Provider Permitted in Active Environment?
76
+ if (provider.type === 'COMMERCIAL') {
77
+ const envPolicy = checkEnvironmentCommercialPolicy(envTier, options);
78
+ if (!envPolicy.allowed) {
79
+ return createGateDecision({
80
+ authorized: false,
81
+ decision: 'DENY',
82
+ reasonCode: envPolicy.reasonCode,
83
+ message: envPolicy.message,
84
+ context: { provider_id: providerId, capability_id: capabilityId, environment: envTier }
85
+ });
86
+ }
87
+ }
88
+
89
+ // 3. Is Capability Declared for this Provider?
90
+ if (!capabilityId || !registry.hasCapability(providerId, capabilityId)) {
91
+ return createGateDecision({
92
+ authorized: false,
93
+ decision: 'DENY',
94
+ reasonCode: 'PROVIDER_CAPABILITY_UNDECLARED',
95
+ message: 'Capability "' + (capabilityId || '') + '" is not declared for provider "' + providerId + '".',
96
+ context: { provider_id: providerId, capability_id: capabilityId, environment: envTier }
97
+ });
98
+ }
99
+
100
+ // 4. Does Adapter Support Capability?
101
+ if (request.adapter) {
102
+ const hasAdapterSupport = typeof request.adapter.supportsCapability === 'function'
103
+ ? request.adapter.supportsCapability(capabilityId)
104
+ : (Array.isArray(request.adapter.supportedCapabilities) && request.adapter.supportedCapabilities.includes(capabilityId));
105
+
106
+ if (!hasAdapterSupport) {
107
+ return createGateDecision({
108
+ authorized: false,
109
+ decision: 'DENY',
110
+ reasonCode: 'PROVIDER_CAPABILITY_UNSUPPORTED',
111
+ message: 'Adapter for provider "' + providerId + '" does not implement capability "' + capabilityId + '".',
112
+ context: { provider_id: providerId, capability_id: capabilityId, environment: envTier }
113
+ });
114
+ }
115
+ }
116
+
117
+ // 5. Success
118
+ return createGateDecision({
119
+ authorized: true,
120
+ decision: 'ALLOW',
121
+ reasonCode: 'CAPABILITY_VERIFIED',
122
+ message: 'Provider "' + providerId + '" capability "' + capabilityId + '" verified successfully.',
123
+ context: { provider_id: providerId, capability_id: capabilityId, environment: envTier }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * BillableActionGate: Evaluates spending authorization, cost state classification,
129
+ * budget limits, and fail-closed commercial defaults.
130
+ *
131
+ * @param {object} request
132
+ * @param {string} request.action_id
133
+ * @param {string} request.provider_id
134
+ * @param {string} request.capability_id
135
+ * @param {string} [request.environment]
136
+ * @param {number} [request.requested_units=1]
137
+ * @param {object} [request.authorization_token]
138
+ * @param {object} ledger - Loaded cost-ledger object or ProviderRegistry
139
+ * @param {object} [options={}]
140
+ * @param {string[]} [options.declaredActions] - Optional whitelist of declared actions in spec.md
141
+ * @returns {object} Structured gate decision
142
+ */
143
+ function evaluateBillableAction(request, ledger, options = {}) {
144
+ if (!request || typeof request !== 'object') {
145
+ return createGateDecision({
146
+ authorized: false,
147
+ decision: 'DENY',
148
+ reasonCode: 'UNDECLARED_BILLABLE_ACTION',
149
+ message: 'Billable action request must be an object.',
150
+ context: {}
151
+ });
152
+ }
153
+
154
+ const actionId = request.action_id;
155
+ const providerId = (request.provider_id || '').toLowerCase().trim();
156
+ const capabilityId = request.capability_id;
157
+ const envTier = resolveEnvironmentTier({ environment: request.environment, ...options });
158
+ const requestedUnits = typeof request.requested_units === 'number' ? request.requested_units : 1;
159
+
160
+ // 1. Action Declaration Check (Must be explicitly declared)
161
+ if (!actionId || typeof actionId !== 'string' || !actionId.trim()) {
162
+ return createGateDecision({
163
+ authorized: false,
164
+ decision: 'DENY',
165
+ reasonCode: 'UNDECLARED_BILLABLE_ACTION',
166
+ message: 'Action ID is missing or invalid.',
167
+ context: { action_id: actionId, provider_id: providerId, environment: envTier }
168
+ });
169
+ }
170
+
171
+ if (options.declaredActions && Array.isArray(options.declaredActions)) {
172
+ if (!options.declaredActions.includes(actionId)) {
173
+ return createGateDecision({
174
+ authorized: false,
175
+ decision: 'DENY',
176
+ reasonCode: 'UNDECLARED_BILLABLE_ACTION',
177
+ message: 'Billable action "' + actionId + '" is not declared in specification or active plan.',
178
+ context: { action_id: actionId, provider_id: providerId, environment: envTier }
179
+ });
180
+ }
181
+ }
182
+
183
+ // Resolve Provider & Capability from Ledger
184
+ let providerEntry = null;
185
+ let capabilityEntry = null;
186
+
187
+ if (ledger && ledger.providers && ledger.providers[providerId]) {
188
+ providerEntry = ledger.providers[providerId];
189
+ if (providerEntry.capabilities && providerEntry.capabilities[capabilityId]) {
190
+ capabilityEntry = providerEntry.capabilities[capabilityId];
191
+ }
192
+ }
193
+
194
+ // 2. Cost State Resolution (Strict Fail-Closed)
195
+ const costState = capabilityEntry ? capabilityEntry.cost_state : (request.cost_state || 'UNKNOWN');
196
+
197
+ if (costState === 'UNKNOWN' || !['FREE', 'BILLABLE', 'POTENTIALLY_BILLABLE'].includes(costState)) {
198
+ return createGateDecision({
199
+ authorized: false,
200
+ decision: 'DENY',
201
+ reasonCode: 'UNKNOWN_COST_CLASSIFICATION',
202
+ message: 'Action "' + actionId + '" has UNKNOWN cost classification. Fail-closed: execution denied.',
203
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: 'UNKNOWN', environment: envTier }
204
+ });
205
+ }
206
+
207
+ // 3. Environment Check for Commercial / Billable
208
+ const isCommercial = providerEntry ? providerEntry.type === 'COMMERCIAL' : (costState !== 'FREE');
209
+ if (isCommercial) {
210
+ const envPolicy = checkEnvironmentCommercialPolicy(envTier, options);
211
+ if (!envPolicy.allowed) {
212
+ return createGateDecision({
213
+ authorized: false,
214
+ decision: 'DENY',
215
+ reasonCode: envPolicy.reasonCode,
216
+ message: envPolicy.message,
217
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: costState, environment: envTier }
218
+ });
219
+ }
220
+ }
221
+
222
+ // 4. FREE Actions (Permitted for Local / MOCK / Zero Cost)
223
+ if (costState === 'FREE') {
224
+ return createGateDecision({
225
+ authorized: true,
226
+ decision: 'ALLOW',
227
+ reasonCode: 'ACTION_AUTHORIZED_FREE',
228
+ message: 'Action "' + actionId + '" is FREE and authorized for execution.',
229
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: 'FREE', environment: envTier }
230
+ });
231
+ }
232
+
233
+ // 5. BILLABLE / POTENTIALLY_BILLABLE Actions -> Require Explicit Spending Token
234
+ const token = request.authorization_token;
235
+ const isTokenValid = token && (token.granted === true || token.granted_by || token.source === 'CLI_FLAG' || token.max_budget_units !== undefined);
236
+
237
+ if (!isTokenValid) {
238
+ return createGateDecision({
239
+ authorized: false,
240
+ decision: 'DENY',
241
+ reasonCode: 'BILLABLE_ACTION_UNAUTHORIZED',
242
+ message: 'Action "' + actionId + '" on provider "' + providerId + '" requires explicit spending authorization.',
243
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: costState, environment: envTier }
244
+ });
245
+ }
246
+
247
+ // 6. Budget & Unit Threshold Check
248
+ const estimatedUnitCost = (capabilityEntry && typeof capabilityEntry.estimated_unit_cost === 'number') ? capabilityEntry.estimated_unit_cost : 1;
249
+ const estimatedTotalCost = requestedUnits * estimatedUnitCost;
250
+
251
+ if (token && typeof token.max_budget_units === 'number') {
252
+ if (estimatedTotalCost > token.max_budget_units) {
253
+ return createGateDecision({
254
+ authorized: false,
255
+ decision: 'DENY',
256
+ reasonCode: 'BUDGET_THRESHOLD_EXCEEDED',
257
+ message: 'Action "' + actionId + '" estimated cost (' + estimatedTotalCost + ') exceeds authorized budget limit (' + token.max_budget_units + ').',
258
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: costState, environment: envTier, estimated_total_cost: estimatedTotalCost, max_budget_units: token.max_budget_units }
259
+ });
260
+ }
261
+ }
262
+
263
+ // 7. Full Authorization Pass
264
+ return createGateDecision({
265
+ authorized: true,
266
+ decision: 'ALLOW',
267
+ reasonCode: 'ACTION_AUTHORIZED',
268
+ message: 'Action "' + actionId + '" authorized for execution within budget.',
269
+ context: { action_id: actionId, provider_id: providerId, capability_id: capabilityId, cost_state: costState, environment: envTier, estimated_total_cost: estimatedTotalCost }
270
+ });
271
+ }
272
+
273
+ module.exports = {
274
+ createGateDecision,
275
+ evaluateProviderCapability,
276
+ evaluateBillableAction
277
+ };