@cat-factory/app 0.200.3 → 0.201.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 (30) hide show
  1. package/app/components/foundational/FoundationalContractSummary.vue +14 -2
  2. package/app/components/foundational/FoundationalServiceCatalogList.vue +10 -6
  3. package/app/components/foundational/FoundationalServiceManager.vue +14 -4
  4. package/app/components/foundational/FoundationalServiceSources.vue +19 -2
  5. package/app/components/foundational/FoundationalSuppressions.vue +32 -8
  6. package/app/components/github/BranchProtectionPreflight.vue +202 -0
  7. package/app/components/github/GitHubPanel.vue +14 -0
  8. package/app/components/layout/AccountRunCredentialSettings.vue +97 -0
  9. package/app/components/layout/AccountTeamSettings.vue +9 -0
  10. package/app/components/settings/ConnectionWarnings.vue +12 -2
  11. package/app/components/settings/UserSecretsSection.vue +17 -2
  12. package/app/components/settings/WorkspaceSettingsPanel.vue +33 -0
  13. package/app/composables/api/foundationalServices.ts +20 -11
  14. package/app/composables/api/github.ts +6 -0
  15. package/app/stores/foundationalServices.spec.ts +29 -7
  16. package/app/stores/foundationalServices.ts +15 -12
  17. package/app/stores/workspaceSettings.ts +3 -0
  18. package/app/types/foundationalServices.ts +1 -0
  19. package/app/utils/connectionWarnings.ts +6 -0
  20. package/i18n/locales/de.json +57 -4
  21. package/i18n/locales/en.json +57 -4
  22. package/i18n/locales/es.json +57 -4
  23. package/i18n/locales/fr.json +57 -4
  24. package/i18n/locales/he.json +57 -4
  25. package/i18n/locales/it.json +57 -4
  26. package/i18n/locales/ja.json +57 -4
  27. package/i18n/locales/pl.json +57 -4
  28. package/i18n/locales/tr.json +57 -4
  29. package/i18n/locales/uk.json +57 -4
  30. package/package.json +2 -2
@@ -174,6 +174,7 @@ const draft = reactive({
174
174
  publishPrVerificationReport: true,
175
175
  artifactRetentionDays: 14,
176
176
  kaizenEnabled: true,
177
+ allowInitiatorPat: true,
177
178
  reviewFrictionMode: 'off' as ReviewFrictionMode,
178
179
  reviewFrictionWarnCount: 3,
179
180
  reviewFrictionBlockCountEnabled: false,
@@ -193,6 +194,7 @@ function hydrate() {
193
194
  draft.publishPrVerificationReport = s.publishPrVerificationReport
194
195
  draft.artifactRetentionDays = s.artifactRetentionDays
195
196
  draft.kaizenEnabled = s.kaizenEnabled
197
+ draft.allowInitiatorPat = s.allowInitiatorPat
196
198
  draft.reviewFrictionMode = s.reviewFrictionMode
197
199
  draft.reviewFrictionWarnCount = s.reviewFrictionWarnCount
198
200
  // The hard-block knobs are nullable (null ⇒ that trigger is off); a per-trigger checkbox is
@@ -249,6 +251,7 @@ async function save() {
249
251
  publishPrVerificationReport: draft.publishPrVerificationReport,
250
252
  artifactRetentionDays: draft.artifactRetentionDays,
251
253
  kaizenEnabled: draft.kaizenEnabled,
254
+ allowInitiatorPat: draft.allowInitiatorPat,
252
255
  reviewFrictionMode: draft.reviewFrictionMode,
253
256
  reviewFrictionWarnCount: draft.reviewFrictionWarnCount,
254
257
  reviewFrictionBlockCount: blockCount,
@@ -484,6 +487,36 @@ async function save() {
484
487
  </label>
485
488
  </section>
486
489
 
490
+ <!-- Run credential: the App installation vs. the initiator's own token -->
491
+ <section class="space-y-2">
492
+ <h3 class="text-sm font-semibold text-slate-200">
493
+ {{ t('settings.workspaceSettings.runCredential.heading') }}
494
+ </h3>
495
+ <p class="text-[11px] text-slate-400">
496
+ {{ t('settings.workspaceSettings.runCredential.body') }}
497
+ </p>
498
+ <label class="flex items-center gap-2">
499
+ <USwitch
500
+ v-model="draft.allowInitiatorPat"
501
+ size="sm"
502
+ data-testid="allow-initiator-pat"
503
+ />
504
+ <span class="text-sm text-slate-200">{{
505
+ t('settings.workspaceSettings.runCredential.toggle')
506
+ }}</span>
507
+ </label>
508
+ <p v-if="!draft.allowInitiatorPat" class="text-[11px] text-amber-300">
509
+ {{ t('settings.workspaceSettings.runCredential.offHint') }}
510
+ </p>
511
+ <!-- Stated rather than read: the account floor lives behind an ACCOUNT-admin
512
+ endpoint, which a workspace admin may not hold, so probing it here would 403
513
+ for exactly the people this note is for. A static line sets the expectation
514
+ without pretending to report a value we cannot see. -->
515
+ <p v-if="draft.allowInitiatorPat" class="text-[11px] text-slate-500">
516
+ {{ t('settings.workspaceSettings.runCredential.accountFloorNote') }}
517
+ </p>
518
+ </section>
519
+
487
520
  <!-- Kaizen agent -->
488
521
  <section class="space-y-2">
489
522
  <h3 class="text-sm font-semibold text-slate-200">
@@ -27,10 +27,11 @@ import type { ApiContext } from './context'
27
27
  * capabilities an organisation already runs, which an Architect designs against instead of
28
28
  * proposing a rebuild.
29
29
  *
30
- * Tiered exactly like the prompt-fragment library (`account` `workspace`), so every route
31
- * reuses the same `scope(kind, id)` prefix. Three of them are workspace-only, because they are
32
- * about a tier having something ABOVE it: the merged catalog, and the suppress/restore pair
33
- * that opts a board out of an inherited account service.
30
+ * Tiered exactly like the prompt-fragment library (a deployment's code-registered `builtin`
31
+ * services ⊕ `account` ⊕ `workspace`), so every route reuses the same `scope(kind, id)` prefix.
32
+ * Only the merged catalog + the lazy document read are workspace-only, because a board is what
33
+ * runs agents; the suppress/restore pair serves both scopes, since an account inherits the
34
+ * deployment's registered tier exactly as a board inherits its account's.
34
35
  */
35
36
  export function foundationalServicesApi({ send, ws, scope }: ApiContext) {
36
37
  return {
@@ -82,21 +83,29 @@ export function foundationalServicesApi({ send, ws, scope }: ApiContext) {
82
83
  pathParams: { serviceId },
83
84
  }),
84
85
 
85
- // ---- opting a board out of an inherited account service ---------------
86
+ // ---- opting a tier out of what it INHERITS ----------------------------
86
87
  // The LIST is what makes the pair usable: a suppressed id is by construction absent from the
87
88
  // merged catalog, so nothing else can tell the surface what to offer a restore for.
88
- listFoundationalServiceSuppressions: (workspaceId: string) =>
89
- send(listFoundationalServiceSuppressionsContract, { pathPrefix: ws(workspaceId) }),
89
+ listFoundationalServiceSuppressions: (kind: FoundationalServiceOwnerKind, id: string) =>
90
+ send(listFoundationalServiceSuppressionsContract, { pathPrefix: scope(kind, id) }),
90
91
 
91
- suppressFoundationalService: (workspaceId: string, serviceId: string) =>
92
+ suppressFoundationalService: (
93
+ kind: FoundationalServiceOwnerKind,
94
+ id: string,
95
+ serviceId: string,
96
+ ) =>
92
97
  send(suppressFoundationalServiceContract, {
93
- pathPrefix: ws(workspaceId),
98
+ pathPrefix: scope(kind, id),
94
99
  pathParams: { serviceId },
95
100
  }),
96
101
 
97
- restoreFoundationalService: (workspaceId: string, serviceId: string) =>
102
+ restoreFoundationalService: (
103
+ kind: FoundationalServiceOwnerKind,
104
+ id: string,
105
+ serviceId: string,
106
+ ) =>
98
107
  send(restoreFoundationalServiceContract, {
99
- pathPrefix: ws(workspaceId),
108
+ pathPrefix: scope(kind, id),
100
109
  pathParams: { serviceId },
101
110
  }),
102
111
 
@@ -12,6 +12,7 @@ import {
12
12
  listGitHubInstallationsContract,
13
13
  listGitHubIssuesContract,
14
14
  listGitHubPullsContract,
15
+ checkGitHubBranchProtectionContract,
15
16
  listGitHubReposContract,
16
17
  listGitHubRepoFilesContract,
17
18
  listGitHubRepoTreeContract,
@@ -66,6 +67,11 @@ export function githubApi({ send, ws }: ApiContext) {
66
67
  listGitHubRepos: (workspaceId: string) =>
67
68
  send(listGitHubReposContract, { pathPrefix: ws(workspaceId) }),
68
69
 
70
+ // The branch-protection preflight: a LIVE probe of each linked repo's default branch, so
71
+ // it is invoked on demand rather than folded into the projection reads above.
72
+ checkGitHubBranchProtection: (workspaceId: string) =>
73
+ send(checkGitHubBranchProtectionContract, { pathPrefix: ws(workspaceId) }),
74
+
69
75
  // Programmatic repo creation (privileged App tier). Only called when the
70
76
  // connection reports `canCreateRepos`; otherwise the UI opens GitHub directly.
71
77
  createGitHubRepo: (workspaceId: string, body: CreateRepoBody) =>
@@ -1,5 +1,8 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import { useFoundationalServicesStore } from '~/stores/foundationalServices'
2
+ import {
3
+ useFoundationalServices,
4
+ useFoundationalServicesStore,
5
+ } from '~/stores/foundationalServices'
3
6
  import { useWorkspaceStore } from '~/stores/workspace'
4
7
 
5
8
  // The two store rules that the surface's correctness rests on, and that neither the backend
@@ -11,10 +14,11 @@ import { useWorkspaceStore } from '~/stores/workspace'
11
14
  // - a suppress/restore must refresh the OPT-OUT list as well as the catalog. The two are
12
15
  // complements — an entry leaves one exactly as it enters the other — so refreshing only the
13
16
  // catalog leaves the way BACK stale, which is the failure the pair exists to prevent.
17
+ // - the ACCOUNT scope has an opt-out list too (it inherits the deployment's `builtin` tier), and
18
+ // it has no merged catalog to piggyback on — so the read must not be gated on that catalog.
14
19
 
15
20
  const SERVICE = {
16
21
  id: 'file-storage',
17
- ownerKind: 'account' as const,
18
22
  name: 'File Storage',
19
23
  summary: 'Stores uploads.',
20
24
  description: '',
@@ -30,11 +34,6 @@ const SERVICE = {
30
34
  omittedOperations: 0,
31
35
  },
32
36
  ],
33
- sourceId: null,
34
- sourcePath: null,
35
- pinnedCommit: null,
36
- createdAt: 1,
37
- updatedAt: 1,
38
37
  }
39
38
 
40
39
  function api(over: Record<string, unknown> = {}) {
@@ -119,3 +118,26 @@ describe('foundational-services store', () => {
119
118
  expect(client.getResolvedFoundationalServices.mock.calls.length).toBe(afterProbe + 2)
120
119
  })
121
120
  })
121
+
122
+ describe('foundational-services store at the ACCOUNT scope', () => {
123
+ it('loads and refreshes the opt-out list even with no merged catalog to read', async () => {
124
+ // An account inherits the deployment's code-registered services, so it can suppress one — but
125
+ // it has no `resolved` read of its own. Gating the suppression list on that read (as it was
126
+ // while only a board inherited anything) leaves an account able to opt out and unable to see,
127
+ // or lift, what it opted out of.
128
+ const client = api()
129
+ vi.stubGlobal('useApi', () => client)
130
+ const store = useFoundationalServices('account', 'acct1')
131
+ await store.probe()
132
+ expect(client.listFoundationalServiceSuppressions).toHaveBeenCalledWith('account', 'acct1')
133
+ expect(client.getResolvedFoundationalServices).not.toHaveBeenCalled()
134
+
135
+ await store.suppress('file-storage')
136
+ expect(client.suppressFoundationalService).toHaveBeenCalledWith(
137
+ 'account',
138
+ 'acct1',
139
+ 'file-storage',
140
+ )
141
+ expect(client.listFoundationalServiceSuppressions).toHaveBeenCalledTimes(2)
142
+ })
143
+ })
@@ -40,7 +40,11 @@ function foundationalServicesSetup(
40
40
  ) {
41
41
  const api = useApi()
42
42
 
43
- /** The merged/resolved catalog only exists at the workspace tier. */
43
+ /**
44
+ * The merged/resolved catalog only exists at the workspace tier — a board is what runs agents.
45
+ * The SUPPRESSION pair is not gated on it: an account inherits the deployment's code-registered
46
+ * `builtin` services exactly as a board inherits its account's, so both tiers can opt out.
47
+ */
44
48
  const hasResolved = kind === 'workspace'
45
49
 
46
50
  /** null = not probed yet; true/false = the catalog is on/off for this deployment. */
@@ -70,7 +74,7 @@ function foundationalServicesSetup(
70
74
 
71
75
  /** How many entries of the merged catalog this board inherits rather than owns. */
72
76
  const inheritedCount = computed(
73
- () => resolved.value.filter((entry) => entry.tier === 'account').length,
77
+ () => resolved.value.filter((entry) => entry.tier !== 'workspace').length,
74
78
  )
75
79
 
76
80
  function requireOwnerId(): string {
@@ -94,9 +98,7 @@ function foundationalServicesSetup(
94
98
  hasResolved
95
99
  ? api.getResolvedFoundationalServices(id)
96
100
  : Promise.resolve([] as ResolvedFoundationalService[]),
97
- hasResolved
98
- ? api.listFoundationalServiceSuppressions(id)
99
- : Promise.resolve([] as FoundationalServiceSuppression[]),
101
+ api.listFoundationalServiceSuppressions(kind, id),
100
102
  ])
101
103
  services.value = tier
102
104
  resolved.value = merged
@@ -133,14 +135,15 @@ function foundationalServicesSetup(
133
135
  }
134
136
 
135
137
  async function refreshResolved() {
136
- if (!hasResolved) return
137
138
  const id = requireOwnerId()
138
139
  // Both in one pass: a write that changes the merge routinely changes the opt-out list too
139
140
  // (suppressing removes an entry from one and adds it to the other), and refreshing only the
140
141
  // catalog would leave the way BACK stale — the exact state the pair exists to avoid.
141
142
  const [merged, opted] = await Promise.all([
142
- api.getResolvedFoundationalServices(id),
143
- api.listFoundationalServiceSuppressions(id),
143
+ hasResolved
144
+ ? api.getResolvedFoundationalServices(id)
145
+ : Promise.resolve([] as ResolvedFoundationalService[]),
146
+ api.listFoundationalServiceSuppressions(kind, id),
144
147
  ])
145
148
  resolved.value = merged
146
149
  suppressions.value = opted
@@ -171,15 +174,15 @@ function foundationalServicesSetup(
171
174
  await reload()
172
175
  }
173
176
 
174
- /** Opt this board out of an inherited ACCOUNT service. Destroys nothing; reversible. */
177
+ /** Opt this tier out of a service it INHERITS. Destroys nothing; reversible. */
175
178
  async function suppress(serviceId: string) {
176
- await api.suppressFoundationalService(requireWorkspaceId(), serviceId)
179
+ await api.suppressFoundationalService(kind, requireOwnerId(), serviceId)
177
180
  await reload()
178
181
  }
179
182
 
180
- /** Lift a suppression, so the board inherits the account service again. */
183
+ /** Lift a suppression, so this tier inherits the service again. */
181
184
  async function restore(serviceId: string) {
182
- await api.restoreFoundationalService(requireWorkspaceId(), serviceId)
185
+ await api.restoreFoundationalService(kind, requireOwnerId(), serviceId)
183
186
  await reload()
184
187
  }
185
188
 
@@ -25,6 +25,9 @@ const DEFAULTS: WorkspaceSettings = {
25
25
  // banner on every board before the snapshot even lands.
26
26
  defaultProvisionType: null,
27
27
  defaultProvisionManifestId: null,
28
+ // On: a run authenticates as its initiator when they stored a GitHub PAT, so pushes and PRs
29
+ // are attributed to them. Off bounds every run to the App installation's scope.
30
+ allowInitiatorPat: true,
28
31
  // The custom metadata bag: empty until someone fills a declared field in. Never null — an
29
32
  // external-tool resolver indexes it (`ctx.metadata.gameId`) with no guard.
30
33
  metadata: {},
@@ -16,6 +16,7 @@ export type {
16
16
  ApiContractFormat,
17
17
  ApiContractSummary,
18
18
  CreateFoundationalServiceInput,
19
+ FolderScanCoverage,
19
20
  FoundationalService,
20
21
  FoundationalServiceOwnerKind,
21
22
  FoundationalServiceSelection,
@@ -14,4 +14,10 @@ export const CONNECTION_WARNING_KEYS: Record<ConnectionWarningCode, string> = {
14
14
  'settings.providerConnection.test.warnings.runner_manifest_no_release',
15
15
  runner_manifest_no_status_path:
16
16
  'settings.providerConnection.test.warnings.runner_manifest_no_status_path',
17
+ github_pat_classic_account_wide:
18
+ 'settings.providerConnection.test.warnings.github_pat_classic_account_wide',
19
+ github_pat_scopes_beyond_need:
20
+ 'settings.providerConnection.test.warnings.github_pat_scopes_beyond_need',
21
+ github_pat_scope_unreadable:
22
+ 'settings.providerConnection.test.warnings.github_pat_scope_unreadable',
17
23
  }
@@ -303,7 +303,10 @@
303
303
  "warningsTitle": "Lücken in dieser Konfiguration",
304
304
  "warnings": {
305
305
  "runner_manifest_no_release": "Kein Release-Template: Beim Abbrechen eines Laufs kann dem Pool nicht mitgeteilt werden, dass er seinen Job stoppen soll. Ein verwaister Job belegt seinen Runner, bis der Pool ihn von selbst zurücknimmt.",
306
- "runner_manifest_no_status_path": "Kein Statuspfad: Jede Abfrage wird als weiterhin laufend gelesen. Ein Job kann daher nur enden, wenn das Abfragebudget des Laufs aufgebraucht ist."
306
+ "runner_manifest_no_status_path": "Kein Statuspfad: Jede Abfrage wird als weiterhin laufend gelesen. Ein Job kann daher nur enden, wenn das Abfragebudget des Laufs aufgebraucht ist.",
307
+ "github_pat_classic_account_wide": "Dies ist ein klassisches Token mit dem Bereich 'repo': Es erreicht jedes Repository, in das du pushen kannst, auch solche, in denen die GitHub-App dieses Arbeitsbereichs nie installiert wurde. Ausführungen, die du startest, nutzen es bevorzugt vor der App. Ein fein abgestuftes Token, das auf die Repositories dieser Installation begrenzt ist, ist enger gefasst.",
308
+ "github_pat_scopes_beyond_need": "Dieses Token gewährt Berechtigungen, die cat-factory nie nutzt. Sie zu entfernen kostet nichts und verkleinert das, was eine kompromittierte Ausführung erreichen könnte.",
309
+ "github_pat_scope_unreadable": "GitHub hat dieses Token akzeptiert, aber keine Bereiche dafür gemeldet, daher lässt sich seine Reichweite hier nicht anzeigen. Prüfe in deinen GitHub-Token-Einstellungen, was es gewährt."
307
310
  }
308
311
  },
309
312
  "toast": {
@@ -407,6 +410,7 @@
407
410
  "optionalField": "{label} (optional)",
408
411
  "testConnection": "Verbindung testen",
409
412
  "tokenValid": "Token gültig",
413
+ "tokenReachTitle": "Was dieses Token erreicht",
410
414
  "tokenRejected": "Token abgelehnt",
411
415
  "secretFallback": "Secret",
412
416
  "toast": {
@@ -848,6 +852,13 @@
848
852
  "body": "Nach Abschluss jeder Ausführung bewertet der Kaizen-Agent, wie jeder Agentenschritt verlief, von reibungslos und effizient bis verwirrt und chaotisch, und empfiehlt Prompt-/Modellverbesserungen. Eine Kombination aus Prompt, Agent und Modell, die fünfmal in Folge hoch bewertet wird und keine Empfehlungen erhält, wird als verifiziert markiert und nicht mehr bewertet. Die Bewertung läuft im Hintergrund und wird in den Ausführungsdetails und im Kaizen-Bildschirm angezeigt. Lege das Modell des Bewerters in der Modellkonfiguration fest (der \"Kaizen\"-Agent).",
849
853
  "toggle": "Agenten-Ausführungen mit Kaizen bewerten"
850
854
  },
855
+ "runCredential": {
856
+ "heading": "Anmeldedaten für Ausführungen",
857
+ "body": "Standardmäßig authentifiziert sich eine Ausführung mit dem persönlichen Zugriffstoken der Person, die sie gestartet hat, sofern sie eines hinterlegt hat. Pushes und Pull Requests werden dann dieser Person zugeordnet. Die Reichweite ihres Tokens ist genau das, was sie ihm gewährt hat, und damit oft größer als die Repositories, die die GitHub-App-Installation dieses Arbeitsbereichs abdeckt. Schalte dies aus, damit sich jede Ausführung stattdessen als App-Installation authentifiziert.",
858
+ "toggle": "Ausführungen im Namen der startenden Person laufen lassen",
859
+ "offHint": "Ausführungen pushen und öffnen Pull Requests als GitHub-App, nicht als die Person, die sie gestartet hat.",
860
+ "accountFloorNote": "Eine kontoweite Richtlinie kann dies für jedes Board verbieten; ist eine gesetzt, verwenden Ausführungen hier unabhängig von diesem Schalter die GitHub-App."
861
+ },
851
862
  "metadata": {
852
863
  "heading": "Arbeitsbereich-Metadaten",
853
864
  "body": "Werte für die benutzerdefinierten Felder dieser Installation. Externe Tools lesen sie und öffnen direkt das richtige Projekt für diesen Arbeitsbereich.",
@@ -1021,6 +1032,15 @@
1021
1032
  "saveFailed": "Konsensgruppe konnte nicht gespeichert werden",
1022
1033
  "deleteFailed": "Konsensgruppe konnte nicht gelöscht werden"
1023
1034
  }
1035
+ },
1036
+ "runCredentialPolicy": {
1037
+ "heading": "Richtlinie für Ausführungs-Anmeldedaten",
1038
+ "body": "Ein Board kann zulassen, dass sich eine Ausführung mit dem persönlichen Zugriffstoken derjenigen Person authentifiziert, die sie gestartet hat. Das ist nützlich, und für eine einzelne Person in einer Organisation, die cat-factory nicht eingeführt hat, ist es oft die einzige Möglichkeit. Ein persönliches Token reicht jedoch so weit, wie seine Besitzerin oder sein Besitzer es erlaubt hat — meist weiter als die GitHub-App-Installation dieses Kontos —, und eine Board-Administration kann den Schalter des Boards wieder einschalten. Verbiete es hier, dann kann es kein Board in diesem Konto mehr.",
1039
+ "toggle": "Ausführungen verbieten, das persönliche Zugriffstoken ihrer Starterin oder ihres Starters zu verwenden",
1040
+ "onHint": "Jede Ausführung in diesem Konto authentifiziert sich als GitHub-App-Installation. Pushes und Pull Requests stammen dann von der App und nicht von der Person, die die Ausführung gestartet hat.",
1041
+ "offHint": "Nicht gesetzt: Jedes Board entscheidet selbst. Bestehende Boards behalten ihre Auswahl.",
1042
+ "saved": "Richtlinie für Ausführungs-Anmeldedaten gespeichert",
1043
+ "saveFailed": "Richtlinie für Ausführungs-Anmeldedaten konnte nicht gespeichert werden"
1024
1044
  }
1025
1045
  },
1026
1046
  "inspector": {
@@ -2329,7 +2349,7 @@
2329
2349
  "action": "Standard wählen"
2330
2350
  },
2331
2351
  "accountFoundational": {
2332
- "intro": "Die gemeinsamen Fähigkeiten, die diese Organisation bereits betreibt - Dateiablage, Benachrichtigungen, Audit - samt ihren API-Verträgen. Jedes Board erbt sie, und einem Architekten wird gesagt, sie zu nutzen statt einen Neubau vorzuschlagen."
2352
+ "intro": "Die gemeinsamen Fähigkeiten, die diese Organisation bereits betreibt - Dateiablage, Benachrichtigungen, Audit - samt ihren API-Verträgen. Jedes Board erbt sie, und einem Architekten wird gesagt, sie zu nutzen statt einen Neubau vorzuschlagen. Dienste, die dieses Deployment im Code registriert, werden hier ebenfalls geerbt und können per ID überschrieben oder unten abgewählt werden."
2333
2353
  }
2334
2354
  },
2335
2355
  "board": {
@@ -4232,6 +4252,7 @@
4232
4252
  "subtitle": "Die gemeinsamen Fähigkeiten, die eure Organisation bereits betreibt, damit ein Entwurf sie nutzt statt sie neu zu bauen."
4233
4253
  },
4234
4254
  "tier": {
4255
+ "builtin": "Deployment",
4235
4256
  "account": "Konto",
4236
4257
  "workspace": "Board"
4237
4258
  },
@@ -4242,6 +4263,8 @@
4242
4263
  },
4243
4264
  "contracts": {
4244
4265
  "omitted": "+{count} weitere nicht aufgeführt",
4266
+ "notIndexed": "Operationen werden für dieses Format nicht indexiert; öffne das Dokument",
4267
+ "noOperations": "deklariert keine Operationen",
4245
4268
  "none": "Kein API-Vertrag hinterlegt. Einem Agenten wird gesagt, dass die Schnittstelle unbekannt ist, statt dass er sie errät."
4246
4269
  },
4247
4270
  "catalog": {
@@ -4252,8 +4275,14 @@
4252
4275
  "hideDocuments": "Vertragsdokumente ausblenden"
4253
4276
  },
4254
4277
  "suppressions": {
4255
- "title": "Auf diesem Board abgewählt",
4256
- "intro": "Diese Kontodienste sind für Entwürfe auf diesem Board ausgeblendet. Nichts wurde gelöscht, und das Konto behält sie für alle anderen Boards.",
4278
+ "title": {
4279
+ "workspace": "Auf diesem Board abgewählt",
4280
+ "account": "Für dieses Konto abgewählt"
4281
+ },
4282
+ "intro": {
4283
+ "workspace": "Diese geerbten Dienste sind für Entwürfe auf diesem Board ausgeblendet. Nichts wurde gelöscht, und alle anderen Boards behalten sie.",
4284
+ "account": "Diese von diesem Deployment registrierten Dienste sind für Entwürfe in allen Boards dieses Kontos ausgeblendet. Nichts wurde gelöscht, und andere Konten behalten sie."
4285
+ },
4257
4286
  "restore": "Wieder anbieten",
4258
4287
  "shadowsNothing": "Unter dieser ID wird derzeit nichts geerbt, diese Abwahl blendet also aktuell nichts aus."
4259
4288
  },
@@ -4346,6 +4375,7 @@
4346
4375
  "synced": "Synchronisiert: {updated} aktualisiert, {removed} entfernt",
4347
4376
  "syncSkipped": "{count} Datei sah wie ein Vertrag aus, war aber nicht verwendbar. | {count} Dateien sahen wie Verträge aus, waren aber nicht verwendbar.",
4348
4377
  "syncTruncated": "Der Ordner enthält mehr, als eine Synchronisierung aufnehmen kann; es wurde nur ein Teil gelesen.",
4378
+ "syncFolderMissing": "Der verknüpfte Ordner ist nicht im Repository vorhanden. Prüfe den Pfad oder verknüpfe die Quelle neu, falls er verschoben wurde.",
4349
4379
  "syncFailed": "Repo konnte nicht synchronisiert werden",
4350
4380
  "changesAvailable": "Stromaufwärts liegen Änderungen vor",
4351
4381
  "upToDate": "Bereits aktuell",
@@ -6066,6 +6096,29 @@
6066
6096
  "title": "cat-factory mit {provider} verbinden",
6067
6097
  "titleAny": "cat-factory mit Ihren Repositorys verbinden",
6068
6098
  "intro": "cat-factory funktioniert, indem es Pull Requests in Ihren Repositorys öffnet. Verbinden Sie Ihren Repository-Anbieter, um fortzufahren."
6099
+ },
6100
+ "branchProtection": {
6101
+ "heading": "Schutz des Standard-Branch",
6102
+ "body": "Agenten-Ausführungen pushen mit einem Zugang, der in jedes abgedeckte Repository schreiben darf. Nichts hier kann verhindern, dass eine kompromittierte Ausführung direkt auf einen Standard-Branch pusht oder ihren eigenen Pull Request über die API des Hosts merged — beides deckt nur der Branch-Schutz beim Host ab, und den richtest du selbst ein. Diese Prüfung zeigt, ob er vorhanden ist.",
6103
+ "check": "Schutz prüfen",
6104
+ "allProtected": "Der Standard-Branch jedes verknüpften Repositories ist geschützt.",
6105
+ "exposed": "{count} Repository hat einen ungeschützten Standard-Branch. | {count} Repositories haben einen ungeschützten Standard-Branch.",
6106
+ "unavailable": "Der verbundene Anbieter kann keinen Branch-Schutz melden, daher sagt diese Prüfung nichts aus — das ist kein sauberes Ergebnis.",
6107
+ "omitted": "{count} weiteres verknüpftes Repository wurde nicht geprüft. | {count} weitere verknüpfte Repositories wurden nicht geprüft.",
6108
+ "state": {
6109
+ "protected": "Geschützt",
6110
+ "unprotected": "Ungeschützt",
6111
+ "unknown": "Unbekannt"
6112
+ },
6113
+ "unprotectedHint": "der Zugang einer Ausführung kann hier direkt pushen und den eigenen Pull Request mergen",
6114
+ "detailUnavailable": "geschützt, aber die Regel konnte nicht gelesen werden — direkte Pushes sind möglicherweise weiterhin erlaubt",
6115
+ "requiresPr": "Pull Request erforderlich, {reviews} zustimmende Review(s), {checks} erforderliche Prüfung(en)",
6116
+ "noPrRequired": "geschützt, aber kein Pull Request erforderlich",
6117
+ "unknownReason": {
6118
+ "branch_not_found": "der Standard-Branch wurde nicht gefunden — die Repository-Projektion ist möglicherweise veraltet",
6119
+ "forbidden": "der Zugang darf die Branches dieses Repositories nicht lesen",
6120
+ "error": "der Host war nicht erreichbar"
6121
+ }
6069
6122
  }
6070
6123
  },
6071
6124
  "tutorial": {
@@ -2351,7 +2351,7 @@
2351
2351
  "action": "Choose default"
2352
2352
  },
2353
2353
  "accountFoundational": {
2354
- "intro": "The shared capabilities this organisation already runs - file storage, notifications, audit - with their API contracts. Every board inherits them, and an architect is told to consume them instead of proposing a rebuild."
2354
+ "intro": "The shared capabilities this organisation already runs - file storage, notifications, audit - with their API contracts. Every board inherits them, and an architect is told to consume them instead of proposing a rebuild. Services this deployment registers in code are inherited here too, and can be overridden by id or opted out of below."
2355
2355
  }
2356
2356
  },
2357
2357
  "settings": {
@@ -2661,7 +2661,10 @@
2661
2661
  "warningsTitle": "Gaps in this configuration",
2662
2662
  "warnings": {
2663
2663
  "runner_manifest_no_release": "No release template: cancelling a run cannot tell the pool to stop its job, so an orphaned job keeps its runner until the pool reclaims it on its own.",
2664
- "runner_manifest_no_status_path": "No status path: every poll reads as still running, so a job can only end by exhausting the run's poll budget."
2664
+ "runner_manifest_no_status_path": "No status path: every poll reads as still running, so a job can only end by exhausting the run's poll budget.",
2665
+ "github_pat_classic_account_wide": "This is a classic token with the 'repo' scope: it reaches every repository you can push to, including ones this workspace's GitHub App was never installed on. Runs you start use it in preference to the App. A fine-grained token limited to this deployment's repositories is narrower.",
2666
+ "github_pat_scopes_beyond_need": "This token grants permissions cat-factory never uses. Removing them costs nothing and narrows what a compromised run could reach.",
2667
+ "github_pat_scope_unreadable": "GitHub accepted this token but reported no scopes for it, so its reach cannot be shown here. Check what it grants in your GitHub token settings."
2665
2668
  }
2666
2669
  },
2667
2670
  "toast": {
@@ -2765,6 +2768,7 @@
2765
2768
  "optionalField": "{label} (optional)",
2766
2769
  "testConnection": "Test connection",
2767
2770
  "tokenValid": "Token valid",
2771
+ "tokenReachTitle": "What this token reaches",
2768
2772
  "tokenRejected": "Token rejected",
2769
2773
  "secretFallback": "Secret",
2770
2774
  "toast": {
@@ -3215,6 +3219,13 @@
3215
3219
  },
3216
3220
  "toggle": "Grade agent runs with Kaizen"
3217
3221
  },
3222
+ "runCredential": {
3223
+ "heading": "Run credential",
3224
+ "body": "By default a run authenticates with the personal access token of whoever started it, when they have stored one, so pushes and pull requests are attributed to that person. Their token's reach is whatever they granted it, which is often wider than the repositories this workspace's GitHub App installation covers. Turn this off to make every run authenticate as the App installation instead.",
3225
+ "toggle": "Let runs act as the person who started them",
3226
+ "offHint": "Runs will push and open pull requests as the GitHub App, not as their initiator.",
3227
+ "accountFloorNote": "An account-wide policy can forbid this for every board; if one is set, runs here use the GitHub App regardless of this switch."
3228
+ },
3218
3229
  "metadata": {
3219
3230
  "heading": "Workspace metadata",
3220
3231
  "body": "Values for the custom fields this deployment defines. External tools read them, so a tool opens on the right project for this workspace.",
@@ -3388,6 +3399,15 @@
3388
3399
  "saveFailed": "Could not save the consensus group",
3389
3400
  "deleteFailed": "Could not delete the consensus group"
3390
3401
  }
3402
+ },
3403
+ "runCredentialPolicy": {
3404
+ "heading": "Run credential policy",
3405
+ "body": "A board can let a run authenticate with the personal access token of whoever started it. That is useful, and for a single person working inside an org that has not adopted cat-factory it is often the only option. But a personal token reaches whatever its owner granted it, which is usually wider than this account's GitHub App installation, and a board admin can turn the board's own switch back on. Forbid it here and no board in this account can.",
3406
+ "toggle": "Forbid runs from using their initiator’s personal access token",
3407
+ "onHint": "Every run in this account will authenticate as the GitHub App installation. Pushes and pull requests will come from the app rather than from the person who started the run.",
3408
+ "offHint": "Not set: each board decides for itself. Existing boards keep whatever they have chosen.",
3409
+ "saved": "Run credential policy saved",
3410
+ "saveFailed": "Could not save the run credential policy"
3391
3411
  }
3392
3412
  },
3393
3413
  "providers": {
@@ -5440,6 +5460,7 @@
5440
5460
  "subtitle": "The shared capabilities your organisation already runs, so a design consumes them instead of rebuilding them."
5441
5461
  },
5442
5462
  "tier": {
5463
+ "builtin": "Deployment",
5443
5464
  "account": "Account",
5444
5465
  "workspace": "Board"
5445
5466
  },
@@ -5450,6 +5471,8 @@
5450
5471
  },
5451
5472
  "contracts": {
5452
5473
  "omitted": "+{count} more not listed",
5474
+ "notIndexed": "operations are not indexed for this format; open the document",
5475
+ "noOperations": "declares no operations",
5453
5476
  "none": "No API contract attached. An agent will be told the interface is unknown rather than guessing at it."
5454
5477
  },
5455
5478
  "catalog": {
@@ -5460,8 +5483,14 @@
5460
5483
  "hideDocuments": "Hide contract documents"
5461
5484
  },
5462
5485
  "suppressions": {
5463
- "title": "Opted out on this board",
5464
- "intro": "These account services are hidden from designs on this board. Nothing was deleted, and the account keeps them for every other board.",
5486
+ "title": {
5487
+ "workspace": "Opted out on this board",
5488
+ "account": "Opted out for this account"
5489
+ },
5490
+ "intro": {
5491
+ "workspace": "These inherited services are hidden from designs on this board. Nothing was deleted, and every other board keeps them.",
5492
+ "account": "These services registered by this deployment are hidden from designs in every board of this account. Nothing was deleted, and other accounts keep them."
5493
+ },
5465
5494
  "restore": "Offer again",
5466
5495
  "shadowsNothing": "Nothing is inherited under this id right now, so this opt-out currently hides nothing."
5467
5496
  },
@@ -5554,6 +5583,7 @@
5554
5583
  "synced": "Synced: {updated} updated, {removed} removed",
5555
5584
  "syncSkipped": "{count} file looked like a contract but could not be used. | {count} files looked like contracts but could not be used.",
5556
5585
  "syncTruncated": "The folder holds more than one sync can take, so only part of it was read.",
5586
+ "syncFolderMissing": "The linked folder is not in the repository. Check the path, or relink the source if it moved.",
5557
5587
  "syncFailed": "Could not sync the repo",
5558
5588
  "changesAvailable": "Changes available upstream",
5559
5589
  "upToDate": "Already up to date",
@@ -6270,6 +6300,29 @@
6270
6300
  "title": "Connect cat-factory to {provider}",
6271
6301
  "titleAny": "Connect cat-factory to your repositories",
6272
6302
  "intro": "cat-factory works by opening pull requests on your repositories. Connect your repository host to continue."
6303
+ },
6304
+ "branchProtection": {
6305
+ "heading": "Default-branch protection",
6306
+ "body": "Agent runs push with a credential that can write to every repository it covers. Nothing here can stop a compromised run pushing straight to a default branch, or merging its own pull request through the host's API — branch protection on the host is what covers both, and it is yours to configure. This checks whether it is in place.",
6307
+ "check": "Check protection",
6308
+ "allProtected": "Every linked repository's default branch is protected.",
6309
+ "exposed": "{count} repository has an unprotected default branch. | {count} repositories have an unprotected default branch.",
6310
+ "unavailable": "The connected provider cannot report branch protection, so this check has nothing to say — it is not a clean result.",
6311
+ "omitted": "{count} further linked repository was not checked. | {count} further linked repositories were not checked.",
6312
+ "state": {
6313
+ "protected": "Protected",
6314
+ "unprotected": "Unprotected",
6315
+ "unknown": "Unknown"
6316
+ },
6317
+ "unprotectedHint": "a run's credential can push here directly and merge its own pull request",
6318
+ "detailUnavailable": "protected, but the rule could not be read — it may still allow direct pushes",
6319
+ "requiresPr": "pull request required, {reviews} approving review(s), {checks} required check(s)",
6320
+ "noPrRequired": "protected, but no pull request is required",
6321
+ "unknownReason": {
6322
+ "branch_not_found": "the default branch was not found — the repository projection may be stale",
6323
+ "forbidden": "the credential is not allowed to read this repository's branches",
6324
+ "error": "the host could not be reached"
6325
+ }
6273
6326
  }
6274
6327
  },
6275
6328
  "tutorial": {