From 7fe4bba025f45c49976580b7131a4cb024e4d7c0 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 13:24:04 +0100 Subject: [PATCH 1/9] Add endpoint to check team deploy policy and update allowed features for AI --- forge/routes/api/assistant.js | 18 ++++++++++++++++++ forge/routes/api/team.js | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/forge/routes/api/assistant.js b/forge/routes/api/assistant.js index 7c4a4c4614..b007146ab8 100644 --- a/forge/routes/api/assistant.js +++ b/forge/routes/api/assistant.js @@ -121,6 +121,24 @@ module.exports = async function (app) { } } }) + /** + * Endpoint for nr-assistant to check, live, whether the team has opted in to + * agent-initiated deploys. Deliberately not cached/pushed via settings.js - it + * must be checked at the moment a deploy is being considered so that turning the + * team setting off takes effect immediately, without an instance restart. + * @name /api/v1/assistant/deploy-policy + * @static + * @memberof forge.routes.api.assistant + */ + app.get('/deploy-policy', { + schema: { + hide: true // dont show in swagger + } + }, async (request, reply) => { + const isAiEnabled = !!(app.config.features.enabled('ai') && request.team?.getFeatureProperty('ai', true)) + const autoDeploy = isAiEnabled && !!request.team?.getFeatureProperty('agentAutoDeploy', false) + reply.send({ autoDeploy }) + }) /** * Endpoint for FIM (fill-in-the-middle) code completion requests * For now, this is simply a relay to an external assistant service diff --git a/forge/routes/api/team.js b/forge/routes/api/team.js index 6cda6abc2b..c13d46a90b 100644 --- a/forge/routes/api/team.js +++ b/forge/routes/api/team.js @@ -913,7 +913,7 @@ module.exports = async function (app) { } } else if (Object.hasOwn(request.body, 'features')) { // Team owners can update feature overrides (e.g. opt out of AI) - const allowedFeatures = ['ai'] + const allowedFeatures = ['ai', 'agentAutoDeploy'] const requestedFeatures = request.body.features || {} const currentProperties = typeof request.team.properties === 'string' ? JSON.parse(request.team.properties) : (request.team.properties || {}) currentProperties.features = currentProperties.features || {} From ac3ff814952eb3c175921e353602249e6f3b07e9 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 13:30:42 +0100 Subject: [PATCH 2/9] Add AI Flow Deploy settings with toggle for agent-initiated deploy --- frontend/src/pages/team/Settings/Danger.vue | 48 ++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/team/Settings/Danger.vue b/frontend/src/pages/team/Settings/Danger.vue index 92c977c9a0..1e80ce86b3 100644 --- a/frontend/src/pages/team/Settings/Danger.vue +++ b/frontend/src/pages/team/Settings/Danger.vue @@ -56,6 +56,16 @@ + AI Flow Deploy +
+
+
Allow AI agents to deploy flow changes they make on this team's instances, without waiting for a person to click Deploy.
+
Enable AI Features above to use this.
+
+
+ +
+
@@ -68,6 +78,7 @@ import teamApi from '../../../api/team.js' import teamTypesApi from '../../../api/teamTypes.js' import FormHeading from '../../../components/FormHeading.vue' +import { getTeamProperty } from '../../../composables/TeamProperties.js' import alerts from '../../../services/alerts.js' import Dialog from '../../../services/dialog.js' @@ -92,7 +103,8 @@ export default { data () { return { teamTypes: [], - aiEnabledOverride: null + aiEnabledOverride: null, + agentAutoDeployOverride: null } }, computed: { @@ -112,6 +124,17 @@ export default { set (value) { this.aiEnabledOverride = value } + }, + agentAutoDeploy: { + get () { + if (this.agentAutoDeployOverride !== null) { + return this.agentAutoDeployOverride + } + return !!getTeamProperty(this.team, 'features.agentAutoDeploy', false) + }, + set (value) { + this.agentAutoDeployOverride = value + } } }, async created () { @@ -175,6 +198,29 @@ export default { }, () => { this.aiEnabledOverride = null }) + }, + showConfirmAgentAutoDeployToggleDialog () { + const enabling = this.agentAutoDeploy + Dialog.show({ + header: enabling ? 'Enable Agent Initiated Deploy' : 'Disable Agent Initiated Deploy', + kind: enabling ? 'danger' : 'primary', + text: enabling + ? 'Are you sure you want to allow AI agents to deploy flow changes they make on this team\'s instances, without a person clicking Deploy?' + : 'Are you sure you want to prevent AI agents from deploying flow changes automatically? Changes they make will still need to be deployed manually.', + confirmLabel: enabling ? 'Enable' : 'Disable' + }, () => { + teamApi.updateTeam(this.team.id, { features: { agentAutoDeploy: enabling } }).then(() => { + alerts.emit(`Agent initiated deploy ${enabling ? 'enabled' : 'disabled'}`, 'confirmation') + this.agentAutoDeployOverride = null + useContextStore().refreshTeam() + }).catch(err => { + alerts.emit('Problem updating agent initiated deploy settings', 'warning') + this.agentAutoDeployOverride = null + console.warn(err) + }) + }, () => { + this.agentAutoDeployOverride = null + }) } } } From 3fab902215ba1f3f8bd0df99cfdda091027732a5 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 14:22:11 +0100 Subject: [PATCH 3/9] Add agent auto deploy feature to context store --- frontend/src/stores/context.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/src/stores/context.js b/frontend/src/stores/context.js index 4e2002fdea..a73203538c 100644 --- a/frontend/src/stores/context.js +++ b/frontend/src/stores/context.js @@ -49,6 +49,19 @@ export const useContextStore = defineStore('context', { } return getTeamProperty(this.team, 'trial.runtimesLimit') ?? null }, + // Mirrors Team.getFeatureProperty on the backend (team override falling back to the + // TeamType/plan default), gated on both the platform `ai` feature and the team's own `ai` + // opt-out - matching what the live check `/api/v1/assistant/deploy-policy` actually gates + // on (`isAiEnabled = platform 'ai' && team.getFeatureProperty('ai', true)`). + agentAutoDeployEnabled () { + if (!this.team) { + return false + } + const platformAiEnabled = useAccountSettingsStore().featuresCheck?.isAiFeatureEnabledForPlatform + const teamAiEnabled = getTeamProperty(this.team, 'features.ai', true) + const agentAutoDeploy = getTeamProperty(this.team, 'features.agentAutoDeploy', false) + return !!(platformAiEnabled && teamAiEnabled && agentAutoDeploy) + }, editorEntityType (state) { const name = state.route?.name if (name?.startsWith('instance-editor')) return 'instance' @@ -98,6 +111,7 @@ export const useContextStore = defineStore('context', { teamId: this.team?.id || null, teamSlug: this.team?.slug || null, telemetryEnabled: useAccountSettingsStore().featuresCheck?.isTelemetryEnabled ?? false, + agentAutoDeployEnabled: this.agentAutoDeployEnabled, instanceId: null, deviceId: null, applicationId: null, @@ -142,6 +156,7 @@ export const useContextStore = defineStore('context', { teamId: this.team?.id || null, teamSlug: this.team?.slug || null, telemetryEnabled: useAccountSettingsStore().featuresCheck?.isTelemetryEnabled ?? false, + agentAutoDeployEnabled: this.agentAutoDeployEnabled, instanceId: state.instance ? state.instance.id : null, deviceId: state.device ? state.device.id : null, applicationId: this.application ? this.application.id : null, From bd54768e59321cb0da460f8f229f2119e8d64ca0 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 14:27:39 +0100 Subject: [PATCH 4/9] add tests for agent deploy --- test/unit/forge/routes/api/assistant_spec.js | 75 ++++++++++++++++++++ test/unit/forge/routes/api/team_spec.js | 56 +++++++++++++++ test/unit/frontend/stores/context.spec.js | 64 +++++++++++++++++ 3 files changed, 195 insertions(+) diff --git a/test/unit/forge/routes/api/assistant_spec.js b/test/unit/forge/routes/api/assistant_spec.js index aeffe9b831..7d488e66d0 100644 --- a/test/unit/forge/routes/api/assistant_spec.js +++ b/test/unit/forge/routes/api/assistant_spec.js @@ -560,6 +560,81 @@ describe('Assistant API', async function () { }) }) + describe('deploy-policy endpoint', async function () { + // The default 'starter' TeamType is bootstrapped with enableAllFeatures: true + // (forge/db/controllers/TeamType.js), which makes TeamType.getFeatureProperty return + // true for *any* key regardless of its defaultValue (forge/db/models/TeamType.js). + // Pin it to false for this block so "no team override" actually means "off", matching + // real teams - explicit team-level overrides below are unaffected either way since + // they win before the TeamType is ever consulted. + let originalTeamProperties + let originalTeamTypeProperties + before(async function () { + const defaultTeamType = await app.db.models.TeamType.findOne({ where: { name: 'starter' } }) + originalTeamTypeProperties = JSON.parse(JSON.stringify(defaultTeamType.properties)) + await enableTeamTypeFeatureFlag(app, false, 'agentAutoDeploy') + }) + after(async function () { + const defaultTeamType = await app.db.models.TeamType.findOne({ where: { name: 'starter' } }) + defaultTeamType.properties = originalTeamTypeProperties + await defaultTeamType.save() + }) + beforeEach(async function () { + originalTeamProperties = TestObjects.ATeam.properties + }) + afterEach(async function () { + TestObjects.ATeam.properties = originalTeamProperties + await TestObjects.ATeam.save() + }) + it('reports autoDeploy false by default', async function () { + const response = await app.inject({ + method: 'GET', + url: '/api/v1/assistant/deploy-policy', + headers: { authorization: 'Bearer ' + TestObjects.tokens.instance } + }) + response.statusCode.should.equal(200) + response.json().should.have.property('autoDeploy', false) + }) + it('reports autoDeploy true when the team has enabled it and ai is enabled', async function () { + TestObjects.ATeam.properties = { features: { agentAutoDeploy: true } } + await TestObjects.ATeam.save() + const response = await app.inject({ + method: 'GET', + url: '/api/v1/assistant/deploy-policy', + headers: { authorization: 'Bearer ' + TestObjects.tokens.instance } + }) + response.statusCode.should.equal(200) + response.json().should.have.property('autoDeploy', true) + }) + it('reports autoDeploy false when agentAutoDeploy is enabled but the team has opted out of ai', async function () { + TestObjects.ATeam.properties = { features: { agentAutoDeploy: true, ai: false } } + await TestObjects.ATeam.save() + const response = await app.inject({ + method: 'GET', + url: '/api/v1/assistant/deploy-policy', + headers: { authorization: 'Bearer ' + TestObjects.tokens.instance } + }) + response.statusCode.should.equal(200) + response.json().should.have.property('autoDeploy', false) + }) + it('reports autoDeploy false when the platform ai feature is disabled', async function () { + TestObjects.ATeam.properties = { features: { agentAutoDeploy: true } } + await TestObjects.ATeam.save() + app.config.features.register('ai', false, true) + try { + const response = await app.inject({ + method: 'GET', + url: '/api/v1/assistant/deploy-policy', + headers: { authorization: 'Bearer ' + TestObjects.tokens.instance } + }) + response.statusCode.should.equal(200) + response.json().should.have.property('autoDeploy', false) + } finally { + app.config.features.register('ai', true, true) + } + }) + }) + describe('assets endpoint', async function () { const assetUrl1 = '/api/v1/assistant/assets/model.json' const assetUrl2 = '/api/v1/assistant/assets/model.bin' diff --git a/test/unit/forge/routes/api/team_spec.js b/test/unit/forge/routes/api/team_spec.js index 1b5fdfb847..024f0c67d9 100644 --- a/test/unit/forge/routes/api/team_spec.js +++ b/test/unit/forge/routes/api/team_spec.js @@ -1630,6 +1630,62 @@ describe('Team API', function () { team.should.have.property('TeamTypeId', newTeamType.id) }) + describe('Feature overrides (features branch)', async function () { + // PUT /api/v1/teams/:teamId { features: {...} } + it('owner can toggle agentAutoDeploy', async function () { + const team = await app.db.models.Team.create({ name: 'update-team-feat-1', slug: 'team-feat-1', TeamTypeId: app.defaultTeamType.id }) + await team.addUser(TestObjects.bob, { through: { role: Roles.Owner } }) + + const response = await app.inject({ + method: 'PUT', + url: `/api/v1/teams/${team.hashid}`, + payload: { + features: { agentAutoDeploy: true } + }, + cookies: { sid: TestObjects.tokens.bob } + }) + response.statusCode.should.equal(200) + + await team.reload() + team.properties.features.should.have.property('agentAutoDeploy', true) + }) + it('member cannot toggle agentAutoDeploy', async function () { + const team = await app.db.models.Team.create({ name: 'update-team-feat-2', slug: 'team-feat-2', TeamTypeId: app.defaultTeamType.id }) + await team.addUser(TestObjects.bob, { through: { role: Roles.Member } }) + + const response = await app.inject({ + method: 'PUT', + url: `/api/v1/teams/${team.hashid}`, + payload: { + features: { agentAutoDeploy: true } + }, + cookies: { sid: TestObjects.tokens.bob } + }) + response.statusCode.should.equal(403) + + await team.reload() + should.not.exist(team.properties?.features?.agentAutoDeploy) + }) + it('ignores feature keys outside the allowlist', async function () { + const team = await app.db.models.Team.create({ name: 'update-team-feat-3', slug: 'team-feat-3', TeamTypeId: app.defaultTeamType.id }) + await team.addUser(TestObjects.bob, { through: { role: Roles.Owner } }) + + const response = await app.inject({ + method: 'PUT', + url: `/api/v1/teams/${team.hashid}`, + payload: { + features: { agentAutoDeploy: true, notAllowed: true } + }, + cookies: { sid: TestObjects.tokens.bob } + }) + response.statusCode.should.equal(200) + + await team.reload() + team.properties.features.should.have.property('agentAutoDeploy', true) + should.not.exist(team.properties.features.notAllowed) + }) + }) + describe('Suspending team', async function () { it('non-owner cannot suspend team', async function () { const teamName = generateName('suspend-team') diff --git a/test/unit/frontend/stores/context.spec.js b/test/unit/frontend/stores/context.spec.js index 8c54e38ab7..1bc5c4350d 100644 --- a/test/unit/frontend/stores/context.spec.js +++ b/test/unit/frontend/stores/context.spec.js @@ -277,6 +277,53 @@ describe('context store', () => { }) }) + describe('agentAutoDeployEnabled', () => { + it('returns false when there is no team', () => { + const store = useContextStore() + expect(store.agentAutoDeployEnabled).toBe(false) + }) + + it('returns false when the platform ai feature is disabled', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: false } + store.setTeam({ properties: { features: { agentAutoDeploy: true } }, type: { properties: { features: {} } } }) + expect(store.agentAutoDeployEnabled).toBe(false) + }) + + it('returns false when the team has opted out of ai', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: true } + store.setTeam({ properties: { features: { agentAutoDeploy: true, ai: false } }, type: { properties: { features: {} } } }) + expect(store.agentAutoDeployEnabled).toBe(false) + }) + + it('returns false when the team has not enabled agentAutoDeploy', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: true } + store.setTeam({ properties: {}, type: { properties: { features: {} } } }) + expect(store.agentAutoDeployEnabled).toBe(false) + }) + + it('returns true when the team override enables it and ai is enabled', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: true } + store.setTeam({ properties: { features: { agentAutoDeploy: true } }, type: { properties: { features: {} } } }) + expect(store.agentAutoDeployEnabled).toBe(true) + }) + + it('falls back to the TeamType default when the team has no override', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: true } + store.setTeam({ properties: {}, type: { properties: { features: { agentAutoDeploy: true } } } }) + expect(store.agentAutoDeployEnabled).toBe(true) + }) + }) + describe('isTrialAccount', () => { it('returns false when team has no billing', () => { const store = useContextStore() @@ -461,6 +508,23 @@ describe('context store', () => { expect(expert.teamSlug).toBe('my-team') }) + // Both branches build the object separately, so a field added to one + // and not the other goes missing depending on load timing + it('carries agentAutoDeployEnabled on both the early-return and main paths', () => { + const store = useContextStore() + const settingsStore = useAccountSettingsStore() + settingsStore.features = { ai: true } + store.setTeam({ properties: { features: { agentAutoDeploy: true } }, type: { properties: { features: {} } } }) + + expect(store.route).toBe(null) + expect(store.expert.agentAutoDeployEnabled).toBe(true) + + store.setTeamMembership({ role: 30 }) + store.updateRoute({ name: 'team', fullPath: '/team/a', params: {} }) + expect(store.route).not.toBe(null) + expect(store.expert.agentAutoDeployEnabled).toBe(true) + }) + it('sets telemetryEnabled from the platform setting', () => { const store = useContextStore() const settingsStore = useAccountSettingsStore() From b0e627c6e16093027aff6f7147da8865a3a5cadc Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 14:30:05 +0100 Subject: [PATCH 5/9] Add optional chaining to team properties access (test suite highlighted a pre-existing issue where team object has no .type) Regresion tests added to cover this --- frontend/src/composables/TeamProperties.js | 4 +-- .../composables/TeamProperties.spec.js | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 test/unit/frontend/composables/TeamProperties.spec.js diff --git a/frontend/src/composables/TeamProperties.js b/frontend/src/composables/TeamProperties.js index 09cf41fd79..e8d68f41db 100644 --- a/frontend/src/composables/TeamProperties.js +++ b/frontend/src/composables/TeamProperties.js @@ -21,10 +21,10 @@ function getProperty (properties, key) { } export function getTeamProperty (team, property, defaultValue) { - const teamValue = getProperty(team.properties, property) + const teamValue = getProperty(team?.properties, property) if (teamValue === undefined) { // No value found in team properties. Check the TeamType properties - return getProperty(team.type.properties, property) ?? defaultValue + return getProperty(team?.type?.properties, property) ?? defaultValue } return teamValue } diff --git a/test/unit/frontend/composables/TeamProperties.spec.js b/test/unit/frontend/composables/TeamProperties.spec.js new file mode 100644 index 0000000000..499dca2251 --- /dev/null +++ b/test/unit/frontend/composables/TeamProperties.spec.js @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest' + +import { getTeamProperty } from '../../../../frontend/src/composables/TeamProperties.js' + +describe('getTeamProperty', () => { + test('returns the team-level override when present', () => { + const team = { properties: { features: { agentAutoDeploy: true } }, type: { properties: { features: { agentAutoDeploy: false } } } } + expect(getTeamProperty(team, 'features.agentAutoDeploy', false)).toBe(true) + }) + + test('falls back to the TeamType property when the team has no override', () => { + const team = { properties: {}, type: { properties: { features: { agentAutoDeploy: true } } } } + expect(getTeamProperty(team, 'features.agentAutoDeploy', false)).toBe(true) + }) + + test('falls back to the default value when neither has the property', () => { + const team = { properties: {}, type: { properties: { features: {} } } } + expect(getTeamProperty(team, 'features.agentAutoDeploy', false)).toBe(false) + }) + + test('does not throw when the team has no type', () => { + const team = { id: 'team-1', slug: 'my-team' } + expect(getTeamProperty(team, 'features.agentAutoDeploy', false)).toBe(false) + }) + + test('does not throw when team is null or undefined', () => { + expect(getTeamProperty(null, 'features.agentAutoDeploy', false)).toBe(false) + expect(getTeamProperty(undefined, 'features.agentAutoDeploy', false)).toBe(false) + }) + + test('resolves a nested dotted path', () => { + const team = { properties: {}, type: { properties: { trial: { runtimesLimit: 3 } } } } + expect(getTeamProperty(team, 'trial.runtimesLimit')).toBe(3) + }) +}) From 9d6330f7c9f88f211e3301a2cda75c26a0df5479 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 17:17:17 +0100 Subject: [PATCH 6/9] Add rate limiting to deploy policy endpoint --- forge/routes/api/assistant.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/forge/routes/api/assistant.js b/forge/routes/api/assistant.js index b007146ab8..9650b1ba51 100644 --- a/forge/routes/api/assistant.js +++ b/forge/routes/api/assistant.js @@ -131,6 +131,18 @@ module.exports = async function (app) { * @memberof forge.routes.api.assistant */ app.get('/deploy-policy', { + config: { + rateLimit: app.config.rate_limits + ? { + hook: 'preHandler', // apply the rate as a preHandler so that session is available + max: 60, // max requests per window + timeWindow: 60000, // 1 minute window + keyGenerator: (request) => { + return request.ownerId || request.ip + } + } + : false + }, schema: { hide: true // dont show in swagger } From 7a5df7fce58376788d3957f99bedd09b6ef1ee4e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 17:21:08 +0100 Subject: [PATCH 7/9] Update autoDeploy logic to require explicit team-level opt-in --- forge/routes/api/assistant.js | 7 +++- frontend/src/pages/team/Settings/Danger.vue | 2 +- test/unit/forge/routes/api/assistant_spec.js | 44 ++++++++++++-------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/forge/routes/api/assistant.js b/forge/routes/api/assistant.js index 9650b1ba51..8912001703 100644 --- a/forge/routes/api/assistant.js +++ b/forge/routes/api/assistant.js @@ -148,7 +148,12 @@ module.exports = async function (app) { } }, async (request, reply) => { const isAiEnabled = !!(app.config.features.enabled('ai') && request.team?.getFeatureProperty('ai', true)) - const autoDeploy = isAiEnabled && !!request.team?.getFeatureProperty('agentAutoDeploy', false) + // agentAutoDeploy authorises unattended deploys, so it must be an explicit team-level + // opt-in - read the team's own override directly rather than through + // Team.getFeatureProperty, which falls back to TeamType.getFeatureProperty and would + // return true for every team on any TeamType bootstrapped with enableAllFeatures (the + // platform default), regardless of whether anyone actually opted in. + const autoDeploy = isAiEnabled && request.team?.properties?.features?.agentAutoDeploy === true reply.send({ autoDeploy }) }) /** diff --git a/frontend/src/pages/team/Settings/Danger.vue b/frontend/src/pages/team/Settings/Danger.vue index 1e80ce86b3..f23f15ff82 100644 --- a/frontend/src/pages/team/Settings/Danger.vue +++ b/frontend/src/pages/team/Settings/Danger.vue @@ -119,7 +119,7 @@ export default { if (this.aiEnabledOverride !== null) { return this.aiEnabledOverride } - return this.team?.type?.properties?.features?.ai !== false + return getTeamProperty(this.team, 'features.ai', true) !== false }, set (value) { this.aiEnabledOverride = value diff --git a/test/unit/forge/routes/api/assistant_spec.js b/test/unit/forge/routes/api/assistant_spec.js index 7d488e66d0..1bcdc2ff79 100644 --- a/test/unit/forge/routes/api/assistant_spec.js +++ b/test/unit/forge/routes/api/assistant_spec.js @@ -561,24 +561,13 @@ describe('Assistant API', async function () { }) describe('deploy-policy endpoint', async function () { - // The default 'starter' TeamType is bootstrapped with enableAllFeatures: true - // (forge/db/controllers/TeamType.js), which makes TeamType.getFeatureProperty return - // true for *any* key regardless of its defaultValue (forge/db/models/TeamType.js). - // Pin it to false for this block so "no team override" actually means "off", matching - // real teams - explicit team-level overrides below are unaffected either way since - // they win before the TeamType is ever consulted. + // agentAutoDeploy authorises unattended deploys, so the route reads the team's own + // properties.features.agentAutoDeploy directly - it must never inherit "true" through + // Team.getFeatureProperty's TeamType fallback, which would make it default-on for + // every team on a TeamType bootstrapped with enableAllFeatures (the platform default - + // see forge/db/controllers/TeamType.js). These tests don't need to touch the TeamType + // at all as a result; the one below that does is there specifically to prove that. let originalTeamProperties - let originalTeamTypeProperties - before(async function () { - const defaultTeamType = await app.db.models.TeamType.findOne({ where: { name: 'starter' } }) - originalTeamTypeProperties = JSON.parse(JSON.stringify(defaultTeamType.properties)) - await enableTeamTypeFeatureFlag(app, false, 'agentAutoDeploy') - }) - after(async function () { - const defaultTeamType = await app.db.models.TeamType.findOne({ where: { name: 'starter' } }) - defaultTeamType.properties = originalTeamTypeProperties - await defaultTeamType.save() - }) beforeEach(async function () { originalTeamProperties = TestObjects.ATeam.properties }) @@ -595,6 +584,27 @@ describe('Assistant API', async function () { response.statusCode.should.equal(200) response.json().should.have.property('autoDeploy', false) }) + it('reports autoDeploy false when the TeamType has enableAllFeatures: true and the team has never opted in', async function () { + const defaultTeamType = await app.db.models.TeamType.findOne({ where: { name: 'starter' } }) + const originalTeamTypeProperties = JSON.parse(JSON.stringify(defaultTeamType.properties)) + try { + const props = defaultTeamType.properties + props.enableAllFeatures = true + defaultTeamType.properties = props + await defaultTeamType.save() + + const response = await app.inject({ + method: 'GET', + url: '/api/v1/assistant/deploy-policy', + headers: { authorization: 'Bearer ' + TestObjects.tokens.instance } + }) + response.statusCode.should.equal(200) + response.json().should.have.property('autoDeploy', false) + } finally { + defaultTeamType.properties = originalTeamTypeProperties + await defaultTeamType.save() + } + }) it('reports autoDeploy true when the team has enabled it and ai is enabled', async function () { TestObjects.ATeam.properties = { features: { agentAutoDeploy: true } } await TestObjects.ATeam.save() From 3f5d74ce7c851c501cbac0dfc4a950f04a4761a7 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 17 Sep 2026 17:21:26 +0100 Subject: [PATCH 8/9] Update dialog headers and alerts for AI Flow Deploy feature --- frontend/src/pages/team/Settings/Danger.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/team/Settings/Danger.vue b/frontend/src/pages/team/Settings/Danger.vue index f23f15ff82..f75070aa89 100644 --- a/frontend/src/pages/team/Settings/Danger.vue +++ b/frontend/src/pages/team/Settings/Danger.vue @@ -202,7 +202,7 @@ export default { showConfirmAgentAutoDeployToggleDialog () { const enabling = this.agentAutoDeploy Dialog.show({ - header: enabling ? 'Enable Agent Initiated Deploy' : 'Disable Agent Initiated Deploy', + header: enabling ? 'Enable AI Flow Deploy' : 'Disable AI Flow Deploy', kind: enabling ? 'danger' : 'primary', text: enabling ? 'Are you sure you want to allow AI agents to deploy flow changes they make on this team\'s instances, without a person clicking Deploy?' @@ -210,11 +210,11 @@ export default { confirmLabel: enabling ? 'Enable' : 'Disable' }, () => { teamApi.updateTeam(this.team.id, { features: { agentAutoDeploy: enabling } }).then(() => { - alerts.emit(`Agent initiated deploy ${enabling ? 'enabled' : 'disabled'}`, 'confirmation') + alerts.emit(`AI Flow Deploy ${enabling ? 'enabled' : 'disabled'}`, 'confirmation') this.agentAutoDeployOverride = null useContextStore().refreshTeam() }).catch(err => { - alerts.emit('Problem updating agent initiated deploy settings', 'warning') + alerts.emit('Problem updating AI Flow Deploy settings', 'warning') this.agentAutoDeployOverride = null console.warn(err) }) From bb71bf7c0fe098aa853b410fbccf96ce1d88be52 Mon Sep 17 00:00:00 2001 From: Andrea Palmieri <76187074+andypalmi@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:49:00 +0200 Subject: [PATCH 9/9] Apply batched suggestions from code review Co-authored-by: Andrea Palmieri <76187074+andypalmi@users.noreply.github.com> --- frontend/src/pages/team/Settings/Danger.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/team/Settings/Danger.vue b/frontend/src/pages/team/Settings/Danger.vue index f75070aa89..ac25193370 100644 --- a/frontend/src/pages/team/Settings/Danger.vue +++ b/frontend/src/pages/team/Settings/Danger.vue @@ -205,8 +205,8 @@ export default { header: enabling ? 'Enable AI Flow Deploy' : 'Disable AI Flow Deploy', kind: enabling ? 'danger' : 'primary', text: enabling - ? 'Are you sure you want to allow AI agents to deploy flow changes they make on this team\'s instances, without a person clicking Deploy?' - : 'Are you sure you want to prevent AI agents from deploying flow changes automatically? Changes they make will still need to be deployed manually.', + ? 'Are you sure you want to allow AI agents to deploy flows on this team\'s instances?' + : 'Are you sure you want to disable AI agents deploying flow changes automatically?', confirmLabel: enabling ? 'Enable' : 'Disable' }, () => { teamApi.updateTeam(this.team.id, { features: { agentAutoDeploy: enabling } }).then(() => {