aidevops 3.22.3 → 3.22.5

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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.22.3-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.22.5-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
@@ -129,6 +129,18 @@ Since the last README feature refresh, aidevops has added or expanded:
129
129
 
130
130
  **Comprehensive DevOps framework with tried & tested services integrations, popular and trusted MCP servers, and enterprise-grade infrastructure quality assurance code monitoring and recommendations.**
131
131
 
132
+ **Vault security model:** aidevops defines protected data classes, provider
133
+ routing labels, trust boundaries, and phased encrypted sync architecture in
134
+ `.agents/reference/vault.md`. Vault guidance is local-first: third-party AI
135
+ providers can only reason over data that has been decrypted into their prompt or
136
+ tool context, so provider-side logs/retention remain outside Vault's technical
137
+ control. Future local LLM mode reduces provider exposure but not local host
138
+ compromise risk.
139
+
140
+ **Vault passphrase warning:** aidevops cannot recover a lost Vault passphrase.
141
+ Save it in a trusted password manager with backups; never paste it into AI chat,
142
+ CLI arguments, environment variables, logs, issue comments, or test fixtures.
143
+
132
144
  ### Report creation, previews, and PDF exports
133
145
 
134
146
  Use aidevops to turn evidence bundles into decision-ready reports while keeping Markdown or JSON as the canonical source. Report agents can produce AI-search audits, SEO/GEO scorecards, delivery reviews, campaign reports, board packs, incident summaries, recurring client handoffs, and before/after remediation evidence.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.22.3
1
+ 3.22.5
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.22.3
8
+ # Version: 3.22.5
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -861,6 +861,7 @@ _help_commands() {
861
861
  echo " review-gate <cmd> Configure review_gate merge policies (rate-limit/completion)"
862
862
  echo " github-app-auth GitHub App auth setup/status and API route decisions"
863
863
  echo " secret <cmd> Manage secrets (set/list/run/init/import/status)"
864
+ echo " vault <cmd> Local encrypted Vault broker (init/unlock/lock/status/read/update)"
864
865
  echo " config <cmd> Feature toggles (list/get/set/reset/path/help)"
865
866
  echo " knowledge <cmd> Knowledge plane management (init/status/provision)"
866
867
  echo " campaign <cmd> Campaign plane: init/provision/ls (P1) + asset (P4) + new/list/status/archive (P2) + draft (P5) + launch/promote (P6)"
@@ -937,6 +938,14 @@ _help_detailed_sections() {
937
938
  echo " aidevops secret import # Import from credentials.sh to gopass"
938
939
  echo " aidevops secret status # Show backend status"
939
940
  echo ""
941
+ echo "Vault:"
942
+ echo " aidevops vault init # Create local encrypted Vault metadata"
943
+ echo " aidevops vault status # Show uninitialized/locked/unlocked/corrupted"
944
+ echo " aidevops vault unlock # Unlock into an in-memory local broker"
945
+ echo " aidevops vault lock # Stop broker and forget in-memory keys"
946
+ echo " aidevops vault read NAME # Read protected data through broker"
947
+ echo " aidevops vault update NAME # Encrypt stdin value through broker"
948
+ echo ""
940
949
  echo "GitHub App Auth:"
941
950
  echo " aidevops github-app-auth status --json # Show active auth mode and budgets"
942
951
  echo " aidevops github-app-auth route issue-list # Explain route decision"
@@ -1055,9 +1064,8 @@ _help_management_sections() {
1055
1064
  echo " aidevops skills categories # List all categories"
1056
1065
  echo ""
1057
1066
  echo "Installation:"
1058
- echo " npm install -g aidevops && aidevops update # via npm (recommended)"
1059
- echo " brew install marcusquinn/tap/aidevops && aidevops update # via Homebrew"
1060
- echo " bash <(curl -fsSL https://aidevops.sh/install) # manual"
1067
+ echo " brew install marcusquinn/tap/aidevops && aidevops update # macOS/Homebrew"
1068
+ echo " npm install -g aidevops && aidevops update # Linux/cross-platform"
1061
1069
  echo ""
1062
1070
  echo "Documentation: https://github.com/marcusquinn/aidevops"
1063
1071
  return 0
@@ -1632,6 +1640,7 @@ main() {
1632
1640
  opencode-sandbox | oc-sandbox) _dispatch_helper "opencode-sandbox-helper.sh" "opencode-sandbox-helper.sh" "$@" ;;
1633
1641
  review-gate | review_gate) _dispatch_helper "review-gate-config-helper.sh" "review-gate-config-helper.sh" "$@" ;;
1634
1642
  secret | secrets) _dispatch_helper "secret-helper.sh" "secret-helper.sh" "$@" ;;
1643
+ vault) _dispatch_helper "vault-helper.sh" "vault-helper.sh" "$@" ;;
1635
1644
  approve) _dispatch_helper "approval-helper.sh" "approval-helper.sh" "$@" ;;
1636
1645
  circuit-breaker | circuit_breaker | cb)
1637
1646
  # Supervisor circuit breaker control (t1331). Bare invocation defaults to status.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.22.3",
3
+ "version": "3.22.5",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -3,6 +3,9 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "bun test tests"
8
+ },
6
9
  "exports": {
7
10
  ".": "./src/app.ts"
8
11
  }
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import { BANNED_ROUTE_PATTERNS, FILE_EXPLORER_ROUTE_MANIFEST, STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
2
+ import { BANNED_ROUTE_PATTERNS, FILE_EXPLORER_ROUTE_MANIFEST, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
3
3
  import { readFileExplorer } from "./file-adapter";
4
- import { readStatus } from "./status-adapter";
4
+ import { readStatus, readVaultStatus } from "./status-adapter";
5
5
 
6
6
  export function createGuiApiApp() {
7
7
  const app = new Hono();
@@ -10,6 +10,10 @@ export function createGuiApiApp() {
10
10
  return context.json(readStatus());
11
11
  });
12
12
 
13
+ app.get(VAULT_STATUS_ROUTE_MANIFEST.route, (context) => {
14
+ return context.json(readVaultStatus());
15
+ });
16
+
13
17
  app.get(FILE_EXPLORER_ROUTE_MANIFEST.route, (context) => {
14
18
  const root = context.req.param("root");
15
19
  const path = context.req.query("path") ?? "";
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import {
6
6
  assertNoSecretSentinels,
7
7
  createEnvelope,
8
+ VAULT_STATUS_ROUTE_MANIFEST,
8
9
  type GuiAiAppSummary,
9
10
  type GuiAiProviderId,
10
11
  type GuiOAuthPoolSummary,
@@ -15,6 +16,7 @@ import {
15
16
  type GuiSettingsSummary,
16
17
  type GuiSetupTargetSummary,
17
18
  type GuiStatusData,
19
+ type GuiVaultStatusData,
18
20
  statusFixture,
19
21
  } from "../../gui-shared/src";
20
22
  import {
@@ -30,6 +32,9 @@ import {
30
32
  stringField,
31
33
  } from "./status-adapter-utils";
32
34
  import { readLocalReposSetupSummary } from "./status-local-repos";
35
+ import { readVaultSummary } from "./status-vault";
36
+
37
+ export { readVaultSummary } from "./status-vault";
33
38
 
34
39
  export interface StatusAdapterOptions {
35
40
  repoRoot?: string;
@@ -47,6 +52,7 @@ export function readStatus(
47
52
  const reposPathRef = "~/.config/aidevops/repos.json";
48
53
  const oauthPoolPathRef = "~/.aidevops/oauth-pool.json";
49
54
  const agentsPathRef = "~/.aidevops/agents";
55
+ const vaultPathRef = statusFixture.vault.path_ref;
50
56
  const settingsPath = expandHome(settingsPathRef);
51
57
  const reposPath = expandHome(reposPathRef);
52
58
  const oauthPoolPath = expandHome(oauthPoolPathRef);
@@ -57,11 +63,13 @@ export function readStatus(
57
63
  const restartRequired = installedVersion !== aidevopsVersion;
58
64
  const setupTargets = readSetupTargets(aidevopsVersion || "unknown", installedVersion || "unknown");
59
65
  const aiApps = readAiApps(aidevopsVersion || "unknown", installedVersion || "unknown");
66
+ const vault = readVaultSummary(repoRoot);
60
67
  const sourcePathRefs = [
61
68
  agentsPathRef,
62
69
  settingsPathRef,
63
70
  reposPathRef,
64
71
  oauthPoolPathRef,
72
+ vaultPathRef,
65
73
  "VERSION",
66
74
  ...setupTargets.map((target) => target.path_ref),
67
75
  ...aiApps.flatMap((app) => [app.app_path_ref, app.binary_path_ref, app.config_path_ref, app.aidevops_target_path_ref]),
@@ -110,6 +118,7 @@ export function readStatus(
110
118
  oauth_pool: readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef),
111
119
  setup_targets: setupTargets,
112
120
  ai_apps: aiApps,
121
+ vault,
113
122
  };
114
123
 
115
124
  const envelope = createEnvelope({
@@ -128,8 +137,27 @@ export function readStatus(
128
137
  return envelope;
129
138
  }
130
139
 
131
- const AI_PROVIDER_IDS: GuiAiProviderId[] = ["anthropic", "openai", "cursor", "google"];
140
+ export function readVaultStatus(
141
+ options: StatusAdapterOptions = {},
142
+ ): GuiResponseEnvelope<GuiVaultStatusData> {
143
+ const data = readVaultSummary(options.repoRoot ?? process.cwd());
144
+ const envelope = createEnvelope({
145
+ operation_id: VAULT_STATUS_ROUTE_MANIFEST.operation_id,
146
+ source: {
147
+ surface: "vault",
148
+ authority: "aidevops vault helper",
149
+ path_refs: [data.path_ref],
150
+ },
151
+ data,
152
+ warnings: ["Vault status exposes metadata only; passphrases, keys, recovery material, and payloads are never returned."],
153
+ observed_at: options.observedAt,
154
+ });
132
155
 
156
+ assertNoSecretSentinels(envelope);
157
+ return envelope;
158
+ }
159
+
160
+ const AI_PROVIDER_IDS: GuiAiProviderId[] = ["anthropic", "openai", "cursor", "google"];
133
161
  const SETUP_TARGET_DEFINITIONS: Array<Pick<GuiSetupTargetSummary, "label" | "path_ref" | "purpose">> = [
134
162
  {
135
163
  label: "Deployed agents",
@@ -0,0 +1,110 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ type GuiVaultSetupState,
6
+ type GuiVaultStatus,
7
+ type GuiVaultStatusData,
8
+ statusFixture,
9
+ } from "../../gui-shared/src";
10
+
11
+ const GUI_VAULT_STATUSES = ["uninitialized", "locked", "unlocked", "corrupted", "unknown"] as const satisfies readonly GuiVaultStatus[];
12
+ const GUI_VAULT_SETUP_STATES = [
13
+ "uninitialized",
14
+ "test-created",
15
+ "restart-required",
16
+ "test-verified",
17
+ "migration-ready",
18
+ "unknown",
19
+ ] as const satisfies readonly GuiVaultSetupState[];
20
+ const INITIALIZED_VAULT_STATUSES = new Set<GuiVaultStatus>(["locked", "unlocked", "corrupted"]);
21
+ const RESTART_TEST_SETUP_STATES = new Set<GuiVaultSetupState>(["test-created", "restart-required", "test-verified"]);
22
+
23
+ export function readVaultSummary(repoRoot: string): GuiVaultStatusData {
24
+ const helperPath = join(repoRoot, ".agents", "scripts", "vault-helper.sh");
25
+ const helperExists = existsSync(helperPath);
26
+ const rawStatus = helperExists ? readVaultCommand(helperPath, ["status"], isGuiVaultStatus) : null;
27
+ const rawSetupState = helperExists ? readVaultCommand(helperPath, ["setup-state"], isGuiVaultSetupState) : null;
28
+ const helperStatus = vaultHelperStatus(helperExists, rawStatus, rawSetupState);
29
+ const status = rawStatus ?? "unknown";
30
+ const setupState = rawSetupState ?? fallbackSetupState(status);
31
+ const unlocked = status === "unlocked";
32
+ const collectionState = vaultCollectionState(status);
33
+
34
+ return {
35
+ ...statusFixture.vault,
36
+ status,
37
+ setup_state: setupState,
38
+ initialized: INITIALIZED_VAULT_STATUSES.has(status),
39
+ locked: !unlocked,
40
+ unlocked,
41
+ available: helperExists && helperStatus !== "error",
42
+ helper_status: helperStatus,
43
+ readiness: {
44
+ ...statusFixture.vault.readiness,
45
+ migration_allowed: unlocked && setupState === "migration-ready",
46
+ setup_required: status === "uninitialized" || setupState === "uninitialized",
47
+ restart_test_required: RESTART_TEST_SETUP_STATES.has(setupState),
48
+ locked_content_hidden: !unlocked,
49
+ },
50
+ collections: statusFixture.vault.collections.map((collection) => ({
51
+ ...collection,
52
+ state: collection.state === "planned" ? "planned" : collectionState,
53
+ })),
54
+ };
55
+ }
56
+
57
+ function readVaultCommand<T extends string>(
58
+ helperPath: string,
59
+ args: string[],
60
+ isAllowed: (value: string) => value is T,
61
+ ): T | null {
62
+ try {
63
+ const output = execFileSync("sh", [helperPath, ...args], {
64
+ encoding: "utf8",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ timeout: 1_000,
67
+ }).trim();
68
+ const firstLine = output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "";
69
+ return isAllowed(firstLine) ? firstLine : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function isGuiVaultStatus(value: string): value is GuiVaultStatus {
76
+ return GUI_VAULT_STATUSES.includes(value as GuiVaultStatus);
77
+ }
78
+
79
+ function isGuiVaultSetupState(value: string): value is GuiVaultSetupState {
80
+ return GUI_VAULT_SETUP_STATES.includes(value as GuiVaultSetupState);
81
+ }
82
+
83
+ function vaultHelperStatus(
84
+ helperExists: boolean,
85
+ status: GuiVaultStatus | null,
86
+ setupState: GuiVaultSetupState | null,
87
+ ): GuiVaultStatusData["helper_status"] {
88
+ if (!helperExists) {
89
+ return "missing";
90
+ }
91
+ return status === null && setupState === null ? "error" : "available";
92
+ }
93
+
94
+ function fallbackSetupState(status: GuiVaultStatus): GuiVaultSetupState {
95
+ return status === "uninitialized" ? "uninitialized" : "unknown";
96
+ }
97
+
98
+ function vaultCollectionState(status: GuiVaultStatus): GuiVaultStatusData["collections"][number]["state"] {
99
+ if (status === "unlocked") {
100
+ return "unlocked";
101
+ }
102
+ if (status === "locked" || status === "corrupted") {
103
+ return "locked";
104
+ }
105
+ if (status === "uninitialized") {
106
+ return "not_configured";
107
+ }
108
+
109
+ return "unknown";
110
+ }
@@ -3,6 +3,9 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "bun test tests"
8
+ },
6
9
  "exports": {
7
10
  ".": "./src/index.ts"
8
11
  }
@@ -1,6 +1,6 @@
1
1
  export type GuiRouteClassification = "read" | "write" | "destructive";
2
2
 
3
- export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read";
3
+ export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read";
4
4
 
5
5
  export type GuiFileRootId = "agents" | "config" | "localSetup" | "git";
6
6
 
@@ -201,6 +201,76 @@ export interface GuiMachineSummary {
201
201
  public_ip: string | null;
202
202
  }
203
203
 
204
+ export type GuiVaultStatus = "uninitialized" | "locked" | "unlocked" | "corrupted" | "unknown";
205
+
206
+ export type GuiVaultSetupState = "uninitialized" | "test-created" | "restart-required" | "test-verified" | "migration-ready" | "unknown";
207
+
208
+ export type GuiVaultEncryptionState = "not_configured" | "locked" | "unlocked" | "planned" | "unknown";
209
+
210
+ export interface GuiVaultCollectionSummary {
211
+ id: string;
212
+ label: string;
213
+ data_class: string;
214
+ labels: string[];
215
+ surface_ids: string[];
216
+ encrypted: boolean;
217
+ state: GuiVaultEncryptionState;
218
+ preview_policy: "hidden_while_locked" | "metadata_only" | "placeholder_only";
219
+ actions_policy: "disabled_while_locked" | "read_only" | "placeholder_disabled";
220
+ }
221
+
222
+ export interface GuiVaultReadinessSummary {
223
+ migration_allowed: boolean;
224
+ setup_required: boolean;
225
+ restart_test_required: boolean;
226
+ remote_unlock_enabled: boolean;
227
+ provider_routing_enforced: boolean;
228
+ locked_content_hidden: boolean;
229
+ }
230
+
231
+ export interface GuiVaultDeviceSummary {
232
+ id_ref: string;
233
+ label: string;
234
+ trust_state: "local" | "pending" | "trusted" | "limited" | "revoked" | "retired" | "planned" | "unknown";
235
+ last_seen: string;
236
+ audit_head_ref: string;
237
+ }
238
+
239
+ export interface GuiVaultSyncSummary {
240
+ status: "not_configured" | "planned" | "ready" | "error" | "unknown";
241
+ transport_policy: "encrypted_only_untrusted_transport";
242
+ encrypted_collections: number;
243
+ pending_requests: number;
244
+ }
245
+
246
+ export interface GuiVaultAuditSummary {
247
+ status: "not_started" | "planned" | "recording" | "error" | "unknown";
248
+ event_count: number;
249
+ latest_event_ref: string;
250
+ }
251
+
252
+ export interface GuiVaultStatusData {
253
+ status: GuiVaultStatus;
254
+ setup_state: GuiVaultSetupState;
255
+ initialized: boolean;
256
+ locked: boolean;
257
+ unlocked: boolean;
258
+ available: boolean;
259
+ helper_status: "available" | "missing" | "error" | "unchecked";
260
+ path_ref: string;
261
+ value_policy: "metadata_only_no_secret_material";
262
+ tooltip: string;
263
+ unlock_hint: string;
264
+ setup_hint: string;
265
+ readiness: GuiVaultReadinessSummary;
266
+ collections: GuiVaultCollectionSummary[];
267
+ devices: GuiVaultDeviceSummary[];
268
+ sync: GuiVaultSyncSummary;
269
+ secure_messages: GuiVaultSyncSummary;
270
+ backups: GuiVaultSyncSummary;
271
+ audit: GuiVaultAuditSummary;
272
+ }
273
+
204
274
  export interface GuiStatusData {
205
275
  aidevops_version: string;
206
276
  update: {
@@ -231,6 +301,7 @@ export interface GuiStatusData {
231
301
  oauth_pool: GuiOAuthPoolSummary;
232
302
  setup_targets: GuiSetupTargetSummary[];
233
303
  ai_apps: GuiAiAppSummary[];
304
+ vault: GuiVaultStatusData;
234
305
  capabilities: GuiCapabilitySummary[];
235
306
  secrets: GuiSecretReference[];
236
307
  placeholders: string[];
@@ -258,6 +329,17 @@ export const FILE_EXPLORER_ROUTE_MANIFEST: GuiRouteManifest = {
258
329
  redactions: ["secret_values", "credential_paths", "private_key_material"],
259
330
  };
260
331
 
332
+ export const VAULT_STATUS_ROUTE_MANIFEST: GuiRouteManifest = {
333
+ route: "/api/vault/status",
334
+ method: "GET",
335
+ operation_id: "vault.status.read",
336
+ classification: "read",
337
+ source_surface: "vault",
338
+ adapter: "statusAdapter.readVaultStatus",
339
+ command_pattern: ["aidevops", "vault", "status"],
340
+ redactions: ["secret_values", "credential_paths", "private_key_material", "vault_passphrases", "recovery_material"],
341
+ };
342
+
261
343
  export const GUI_FILE_ROOTS: readonly GuiFileRootDefinition[] = [
262
344
  {
263
345
  id: "agents",
@@ -320,7 +402,7 @@ export function createEnvelope<TData>(input: {
320
402
  data: input.data,
321
403
  warnings: input.warnings ?? [],
322
404
  errors: input.errors ?? [],
323
- redactions: ["secret_values", "credential_paths", "private_key_material"],
405
+ redactions: ["secret_values", "credential_paths", "private_key_material", "vault_passphrases", "recovery_material"],
324
406
  observed_at: input.observed_at ?? new Date().toISOString(),
325
407
  };
326
408
  }
@@ -42,6 +42,7 @@ export const statusFixture: GuiStatusData = {
42
42
  ],
43
43
  navigation: [
44
44
  ["overview", "Overview", "Local setup, update, and API health."],
45
+ ["vault", "Vault", "Encrypted metadata, setup, sync, and audit."],
45
46
  ["agents", "Agents", "Read-only file explorer for deployed agent files."],
46
47
  ["config", "Config", "Read-only file explorer for aidevops config."],
47
48
  ["git", "Local Repos", "Read-only local git workspace browser."],
@@ -155,6 +156,106 @@ export const statusFixture: GuiStatusData = {
155
156
  needs_update: false,
156
157
  },
157
158
  ],
159
+ vault: {
160
+ status: "uninitialized",
161
+ setup_state: "uninitialized",
162
+ initialized: false,
163
+ locked: true,
164
+ unlocked: false,
165
+ available: true,
166
+ helper_status: "unchecked",
167
+ path_ref: "~/.config/aidevops/vault",
168
+ value_policy: "metadata_only_no_secret_material",
169
+ tooltip: "Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.",
170
+ unlock_hint: "Run `aidevops vault unlock` in a local terminal and enter the passphrase only into the hidden prompt.",
171
+ setup_hint: "Run `aidevops vault init` locally, verify the restart test, then unlock before protected previews or write actions are available.",
172
+ readiness: {
173
+ migration_allowed: false,
174
+ setup_required: true,
175
+ restart_test_required: false,
176
+ remote_unlock_enabled: false,
177
+ provider_routing_enforced: true,
178
+ locked_content_hidden: true,
179
+ },
180
+ collections: [
181
+ {
182
+ id: "vault-status",
183
+ label: "Vault status and setup",
184
+ data_class: "config metadata",
185
+ labels: ["internal", "confidential"],
186
+ surface_ids: ["vault"],
187
+ encrypted: true,
188
+ state: "not_configured",
189
+ preview_policy: "metadata_only",
190
+ actions_policy: "read_only",
191
+ },
192
+ {
193
+ id: "agent-guidance",
194
+ label: "Agent guidance and workflows",
195
+ data_class: "session/history",
196
+ labels: ["confidential"],
197
+ surface_ids: ["agents", "localSetup", "config"],
198
+ encrypted: true,
199
+ state: "not_configured",
200
+ preview_policy: "hidden_while_locked",
201
+ actions_policy: "disabled_while_locked",
202
+ },
203
+ {
204
+ id: "provider-routing",
205
+ label: "Provider routing metadata",
206
+ data_class: "config metadata",
207
+ labels: ["internal", "provider-allowed"],
208
+ surface_ids: ["security", "aiProviders"],
209
+ encrypted: true,
210
+ state: "not_configured",
211
+ preview_policy: "hidden_while_locked",
212
+ actions_policy: "disabled_while_locked",
213
+ },
214
+ {
215
+ id: "fleet-messages",
216
+ label: "Fleet, sync, and secure messages",
217
+ data_class: "mail/messages",
218
+ labels: ["client-confidential"],
219
+ surface_ids: ["devices", "messagingAccounts", "emailAccounts", "inbox", "campaigns", "cases", "projectConfig", "knowledge"],
220
+ encrypted: true,
221
+ state: "planned",
222
+ preview_policy: "hidden_while_locked",
223
+ actions_policy: "disabled_while_locked",
224
+ },
225
+ ],
226
+ devices: [
227
+ {
228
+ id_ref: "local-device",
229
+ label: "Local device",
230
+ trust_state: "local",
231
+ last_seen: "current session",
232
+ audit_head_ref: "not started",
233
+ },
234
+ ],
235
+ sync: {
236
+ status: "planned",
237
+ transport_policy: "encrypted_only_untrusted_transport",
238
+ encrypted_collections: 0,
239
+ pending_requests: 0,
240
+ },
241
+ secure_messages: {
242
+ status: "planned",
243
+ transport_policy: "encrypted_only_untrusted_transport",
244
+ encrypted_collections: 0,
245
+ pending_requests: 0,
246
+ },
247
+ backups: {
248
+ status: "planned",
249
+ transport_policy: "encrypted_only_untrusted_transport",
250
+ encrypted_collections: 0,
251
+ pending_requests: 0,
252
+ },
253
+ audit: {
254
+ status: "planned",
255
+ event_count: 0,
256
+ latest_event_ref: "not started",
257
+ },
258
+ },
158
259
  capabilities: [
159
260
  {
160
261
  id: "setup-status",
@@ -20,6 +20,7 @@
20
20
  "react-icons": "^5.5.0"
21
21
  },
22
22
  "scripts": {
23
- "dev": "vite --host 127.0.0.1"
23
+ "dev": "vite --host 127.0.0.1",
24
+ "test": "bun test tests"
24
25
  }
25
26
  }
@@ -252,6 +252,7 @@ function DesktopStatusBar({ status }: { status: GuiStatusData }): ReactElement {
252
252
  const localRepoCount = status.local_repos.total || status.local_repos.repos.length;
253
253
  const remoteRepoCount = status.repos.total || status.repos.repos.length;
254
254
  const statusLabel = status.update.restart_required ? "Restart required" : "Ready";
255
+ const vaultLabel = status.vault?.unlocked ? "Vault unlocked" : `Vault ${status.vault?.status ?? "unknown"}`;
255
256
 
256
257
  return (
257
258
  <div className="desktop-status-bar" role="status">
@@ -262,6 +263,7 @@ function DesktopStatusBar({ status }: { status: GuiStatusData }): ReactElement {
262
263
  <span>{localRepoCount} local repos</span>
263
264
  <span>{remoteRepoCount} remote repos</span>
264
265
  <span>{status.secrets.length} secrets</span>
266
+ <span>{vaultLabel}</span>
265
267
  <span>{oauthAccounts} provider accounts</span>
266
268
  <span>{needsUpdate} need update</span>
267
269
  </div>
@@ -22,6 +22,7 @@ import {
22
22
  FiLink,
23
23
  FiLink2,
24
24
  FiList,
25
+ FiLock,
25
26
  FiMail,
26
27
  FiMessageSquare,
27
28
  FiMonitor,
@@ -35,6 +36,7 @@ import {
35
36
  } from "react-icons/fi";
36
37
  import type { FontPreference, FontSizePreference, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
37
38
  import { DEFAULT_ACCENT_HUE, dashboardNavItem, fontFamilyForPreference, fontOptions, fontSizeOptions, navGroups, sidebarModeForSurface, surfaceRecordCounts, text } from "./app-model";
39
+ import { VaultPadlock, vaultCollectionForSurface } from "./VaultBadges";
38
40
 
39
41
  const surfaceIcons: Record<SurfaceIconName, IconType> = {
40
42
  apps: FiBox,
@@ -53,6 +55,7 @@ const surfaceIcons: Record<SurfaceIconName, IconType> = {
53
55
  hardDrive: FiHardDrive,
54
56
  link: FiLink,
55
57
  list: FiList,
58
+ lock: FiLock,
56
59
  mail: FiMail,
57
60
  message: FiMessageSquare,
58
61
  note: FiMessageSquare,
@@ -138,9 +141,9 @@ export function Sidebar({ activeSurface, accentHue, canGoBack, canGoForward, fon
138
141
  <nav className="sidebar-content">
139
142
  <SidebarModeTabs mode={sidebarMode} setMode={setSidebarMode} />
140
143
  <ul className="sidebar-top-link">
141
- <SidebarItem activeSurface={activeSurface} item={dashboardNavItem} setActiveSurface={setActiveSurface} showCount={false} />
144
+ <SidebarItem activeSurface={activeSurface} item={dashboardNavItem} setActiveSurface={setActiveSurface} showCount={false} status={status} />
142
145
  </ul>
143
- {visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} />)}
146
+ {visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} status={status} />)}
144
147
  </nav>
145
148
  <SidebarFooter
146
149
  accentHue={accentHue}
@@ -187,32 +190,36 @@ function SidebarModeTabs({ mode, setMode }: {
187
190
  );
188
191
  }
189
192
 
190
- function SidebarGroup({ activeSurface, group, recordCounts, setActiveSurface, showNavCounts }: {
193
+ function SidebarGroup({ activeSurface, group, recordCounts, setActiveSurface, showNavCounts, status }: {
191
194
  activeSurface: SurfaceId;
192
195
  group: SurfaceNavGroup;
193
196
  recordCounts: Partial<Record<SurfaceId, number>>;
194
197
  setActiveSurface: (surface: SurfaceId) => void;
195
198
  showNavCounts: boolean;
199
+ status: GuiStatusData;
196
200
  }) {
197
201
  return (
198
202
  <section className="sidebar-group">
199
203
  <h2>{group.label}</h2>
200
204
  <ul>
201
- {group.items.map((item) => <SidebarItem activeSurface={activeSurface} item={item} key={item.id} recordCount={recordCounts[item.id]} setActiveSurface={setActiveSurface} showCount={showNavCounts} />)}
205
+ {group.items.map((item) => <SidebarItem activeSurface={activeSurface} item={item} key={item.id} recordCount={recordCounts[item.id]} setActiveSurface={setActiveSurface} showCount={showNavCounts} status={status} />)}
202
206
  </ul>
203
207
  </section>
204
208
  );
205
209
  }
206
210
 
207
- function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showCount }: {
211
+ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showCount, status }: {
208
212
  activeSurface: SurfaceId;
209
213
  item: SurfaceNavItem;
210
214
  recordCount?: number;
211
215
  setActiveSurface: (surface: SurfaceId) => void;
212
216
  showCount: boolean;
217
+ status: GuiStatusData;
213
218
  }) {
214
219
  const isActive = activeSurface === item.id;
215
- const tooltip = `${item.label}: ${item.description}`;
220
+ const vaultCollection = vaultCollectionForSurface(status.vault, item.id);
221
+ const vaultTooltip = vaultCollection ? ` ${text.vaultTooltip}` : "";
222
+ const tooltip = `${item.label}: ${item.description}${vaultTooltip}`;
216
223
  const shouldShowCount = showCount && recordCount !== undefined && recordCount > 0;
217
224
 
218
225
  return (
@@ -232,6 +239,7 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
232
239
  {shouldShowCount ? <span className="surface-count">({recordCount})</span> : null}
233
240
  </span>
234
241
  </span>
242
+ {vaultCollection ? <VaultPadlock collection={vaultCollection} compact vault={status.vault} /> : null}
235
243
  {item.badge ? <em>{item.badge}</em> : null}
236
244
  </button>
237
245
  </li>
@@ -6,7 +6,8 @@ import type { SurfaceId, SurfaceNavItem } from "./app-model";
6
6
  import { inventorySurfaceConfigs, text } from "./app-model";
7
7
  import { FileExplorerSurface } from "./FileExplorerSurface";
8
8
  import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
9
- import { AiProvidersSurface, LocalReposSurface, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface } from "./StatusSurfaces";
9
+ import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
10
+ import { isVaultSurfaceLocked, vaultCollectionForSurface } from "./VaultBadges";
10
11
 
11
12
  export function Workspace({ activeItem, activeSectionLabel, activeSurface, fileRoot, status }: {
12
13
  activeItem: SurfaceNavItem;
@@ -19,7 +20,7 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, fileR
19
20
  <section className="app-inset" aria-label={text.workspaceLabel}>
20
21
  <WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} version={displayVersion(status)} />
21
22
  <div className="workspace-scroll">
22
- <SurfaceContent activeSurface={activeSurface} fileRoot={fileRoot} status={status} />
23
+ <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />
23
24
  </div>
24
25
  </section>
25
26
  );
@@ -60,14 +61,17 @@ function displayVersion(status: GuiStatusData): string {
60
61
  return status.aidevops_version;
61
62
  }
62
63
 
63
- function SurfaceContent({ activeSurface, fileRoot, status }: {
64
+ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
65
+ activeItem: SurfaceNavItem;
64
66
  activeSurface: SurfaceId;
65
67
  fileRoot: GuiFileRootId | undefined;
66
68
  status: GuiStatusData;
67
69
  }) {
68
70
  const inventoryConfig = inventorySurfaceConfigs[activeSurface];
71
+ const vaultCollection = vaultCollectionForSurface(status.vault, activeSurface);
69
72
  const staticSurfaces: Partial<Record<SurfaceId, ReactNode>> = {
70
73
  overview: <OverviewSurface status={status} />,
74
+ vault: <VaultSurface status={status} />,
71
75
  routines: <PlannedSurface label={text.routines} detail={text.routineDetail} />,
72
76
  devices: <PlannedSurface label={text.devices} detail={text.devicesIntro} />,
73
77
  vpnsProxies: <PlannedSurface label={text.vpnsProxies} detail={text.vpnsProxiesIntro} />,
@@ -98,6 +102,10 @@ function SurfaceContent({ activeSurface, fileRoot, status }: {
98
102
  aiProviders: <AiProvidersSurface status={status} />,
99
103
  };
100
104
 
105
+ if (isVaultSurfaceLocked(status.vault, activeSurface) && vaultCollection) {
106
+ return <LockedVaultGate collection={vaultCollection} label={activeItem.label} vault={status.vault} />;
107
+ }
108
+
101
109
  if (activeSurface === "git") {
102
110
  return <LocalReposSurface status={status} />;
103
111
  }
@@ -1,9 +1,10 @@
1
1
  /* jshint esversion: 11 */
2
2
  import { useState } from "react";
3
- import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiOAuthProviderSummary, GuiSetupTargetSummary, GuiStatusData } from "../../gui-shared/src";
3
+ import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiOAuthProviderSummary, GuiSetupTargetSummary, GuiStatusData, GuiVaultCollectionSummary, GuiVaultStatusData } from "../../gui-shared/src";
4
4
  import { plannedHomes, text } from "./app-model";
5
5
  import { FileExplorerSurface } from "./FileExplorerSurface";
6
6
  import { PathActions } from "./PathActions";
7
+ import { VaultPadlock } from "./VaultBadges";
7
8
 
8
9
  export function OverviewSurface({ status }: { status: GuiStatusData }) {
9
10
  const metrics = [
@@ -103,6 +104,9 @@ export function SecuritySurface({ status }: { status: GuiStatusData }) {
103
104
  <h2>{text.security}</h2>
104
105
  <p>{text.securityBoundary}</p>
105
106
  </div>
107
+ <div className="notice compact-notice" role="note">
108
+ Vault data policy: Provider AI sends approved plaintext context to a third-party provider; Local AI keeps model processing on this device; Hybrid mode must redact or split protected context before any provider call.
109
+ </div>
106
110
  <ul className="object-list">
107
111
  {status.secrets.map((secret) => (
108
112
  <li key={secret.name}>
@@ -115,6 +119,149 @@ export function SecuritySurface({ status }: { status: GuiStatusData }) {
115
119
  );
116
120
  }
117
121
 
122
+ export function VaultSurface({ status }: { status: GuiStatusData }) {
123
+ const vault = status.vault;
124
+ const vaultCollection = vault.collections.find((collection) => collection.surface_ids.includes("vault")) ?? vault.collections[0];
125
+ const readiness = [
126
+ { label: "migration", value: vault.readiness.migration_allowed ? "ready" : "blocked" },
127
+ { label: "setup", value: vault.readiness.setup_required ? "required" : "done" },
128
+ { label: "restart test", value: vault.readiness.restart_test_required ? "required" : "verified" },
129
+ { label: "remote unlock", value: vault.readiness.remote_unlock_enabled ? "enabled" : "disabled" },
130
+ ];
131
+ const featureCards = [
132
+ { label: text.vaultStatus, value: vault.status, detail: "Metadata-only lock state from the local helper." },
133
+ { label: text.vaultSetup, value: vault.setup_state, detail: vault.setup_hint },
134
+ { label: text.vaultLockUnlock, value: vault.locked ? "locked" : "unlocked", detail: vault.unlock_hint },
135
+ { label: text.vaultDevices, value: `${vault.devices.length} device`, detail: "Device trust metadata only; private keys are never exposed." },
136
+ { label: text.vaultSync, value: vault.sync.status, detail: "Encrypted bundles and signed manifests over untrusted transports." },
137
+ { label: text.vaultMessages, value: vault.secure_messages.status, detail: "Secure message placeholders keep payloads hidden while locked." },
138
+ { label: text.vaultBackups, value: vault.backups.status, detail: "Encrypted backups and recovery flows are metadata-only here." },
139
+ { label: text.vaultAudit, value: vault.audit.status, detail: `${vault.audit.event_count} redacted audit events; ${vault.audit.latest_event_ref}.` },
140
+ ];
141
+
142
+ return (
143
+ <section className="surface-page vault-surface" aria-label={text.vault}>
144
+ <div className="hero-panel vault-hero">
145
+ <div className="section-heading split-heading">
146
+ <div>
147
+ <p className="eyebrow">{vault.value_policy}</p>
148
+ <h2>{text.vault}</h2>
149
+ <p>{text.vaultIntro}</p>
150
+ </div>
151
+ {vaultCollection ? <VaultPadlock collection={vaultCollection} vault={vault} /> : null}
152
+ </div>
153
+ <div className="vault-readiness-strip" aria-label="Vault readiness">
154
+ {readiness.map((item) => <span key={item.label}><strong>{item.label}</strong>{item.value}</span>)}
155
+ </div>
156
+ </div>
157
+ {vault.locked ? (
158
+ <div className="notice compact-notice" role="note">
159
+ {text.vaultLockedPreview} {vault.unlock_hint}
160
+ </div>
161
+ ) : (
162
+ <div className="notice compact-notice" role="note">
163
+ Vault is unlocked for this local session. Protected actions remain read-only until audited write routes are implemented.
164
+ </div>
165
+ )}
166
+ <div className="vault-card-grid">
167
+ {featureCards.map((card) => <VaultFeatureCard detail={card.detail} key={card.label} label={card.label} value={card.value} />)}
168
+ </div>
169
+ <section className="panel vault-setup-panel" aria-label={text.vaultSetup}>
170
+ <div className="section-heading split-heading">
171
+ <div>
172
+ <p className="eyebrow">{text.vaultSetup}</p>
173
+ <h2>{vault.readiness.setup_required ? "Setup required" : "Setup metadata"}</h2>
174
+ <p>{vault.setup_hint}</p>
175
+ </div>
176
+ <button className="secondary-action vault-cta" disabled title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
177
+ </div>
178
+ <ol className="vault-step-list">
179
+ <li>Initialize locally with the hidden-prompt helper.</li>
180
+ <li>Verify the harmless restart test before migrating real data.</li>
181
+ <li>Keep passphrases, recovery material, and private keys out of chat, arguments, environment variables, logs, issues, and fixtures.</li>
182
+ </ol>
183
+ <code>{vault.unlock_hint}</code>
184
+ </section>
185
+ <section className="panel" aria-label="Vault encrypted collections">
186
+ <div className="section-heading">
187
+ <p className="eyebrow">{text.vaultStatus}</p>
188
+ <h2>Encrypted collections</h2>
189
+ <p>{text.vaultCollectionIntro}</p>
190
+ </div>
191
+ <ul className="object-list vault-collection-list">
192
+ {vault.collections.map((collection) => <VaultCollectionRow collection={collection} key={collection.id} vault={vault} />)}
193
+ </ul>
194
+ </section>
195
+ <section className="panel" aria-label="Vault devices and audit">
196
+ <div className="section-heading split-heading">
197
+ <div>
198
+ <p className="eyebrow">{text.vaultDevices}</p>
199
+ <h2>Devices, sync, messages, backups, and audit</h2>
200
+ <p>These placeholders expose readiness and redacted metadata only. Git, object storage, messaging, SSH, VPNs, and VPS disks remain untrusted transports.</p>
201
+ </div>
202
+ <span className="count-pill">{vault.sync.transport_policy}</span>
203
+ </div>
204
+ <div className="vault-device-grid">
205
+ {vault.devices.map((device) => (
206
+ <article className="vault-device-card" key={device.id_ref}>
207
+ <p className="eyebrow">{device.trust_state}</p>
208
+ <h3>{device.label}</h3>
209
+ <Detail label="device" value={device.id_ref} />
210
+ <Detail label="last seen" value={device.last_seen} />
211
+ <Detail label="audit head" value={device.audit_head_ref} />
212
+ </article>
213
+ ))}
214
+ </div>
215
+ </section>
216
+ </section>
217
+ );
218
+ }
219
+
220
+ export function LockedVaultGate({ collection, label, vault }: {
221
+ collection: GuiVaultCollectionSummary;
222
+ label: string;
223
+ vault: GuiVaultStatusData;
224
+ }) {
225
+ return (
226
+ <section className="panel vault-locked-gate" aria-label={`${label} locked by Vault`}>
227
+ <div className="section-heading split-heading">
228
+ <div>
229
+ <p className="eyebrow">{collection.data_class}</p>
230
+ <h2>{label} is locked</h2>
231
+ <p>{text.vaultLockedPreview}</p>
232
+ </div>
233
+ <VaultPadlock collection={collection} vault={vault} />
234
+ </div>
235
+ <div className="notice compact-notice" role="note">
236
+ {text.vaultTooltip} {vault.unlock_hint}
237
+ </div>
238
+ <button className="secondary-action vault-cta" disabled title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
239
+ </section>
240
+ );
241
+ }
242
+
243
+ function VaultFeatureCard({ detail, label, value }: { detail: string; label: string; value: string }) {
244
+ return (
245
+ <article className="vault-feature-card">
246
+ <span>{label}</span>
247
+ <strong>{value}</strong>
248
+ <small>{detail}</small>
249
+ </article>
250
+ );
251
+ }
252
+
253
+ function VaultCollectionRow({ collection, vault }: { collection: GuiVaultCollectionSummary; vault: GuiVaultStatusData }) {
254
+ return (
255
+ <li>
256
+ <strong>{collection.label}</strong>
257
+ <VaultPadlock collection={collection} compact vault={vault} />
258
+ <span>{collection.preview_policy}</span>
259
+ <small>{collection.labels.join(", ")}</small>
260
+ <small>{collection.surface_ids.join(", ")}</small>
261
+ </li>
262
+ );
263
+ }
264
+
118
265
  export function LocalReposSurface({ status }: { status: GuiStatusData }) {
119
266
  const [activeTab, setActiveTab] = useState<"setup" | "files">("setup");
120
267
 
@@ -309,6 +456,9 @@ export function AiProvidersSurface({ status }: { status: GuiStatusData }) {
309
456
  </div>
310
457
  <span className="count-pill">{status.oauth_pool.value_policy}</span>
311
458
  </div>
459
+ <div className="notice compact-notice" role="note">
460
+ Data-leaves-device warning: provider accounts can process prompts outside this machine. Use `data_classification` and `runtime_policy` metadata to require Local AI or explicit provider approval for confidential work.
461
+ </div>
312
462
  <div className="provider-grid">
313
463
  {status.oauth_pool.providers.map((provider) => <ProviderCard key={provider.provider} provider={provider} />)}
314
464
  </div>
@@ -0,0 +1,39 @@
1
+ import type { GuiVaultCollectionSummary, GuiVaultStatusData } from "@aidevops/gui-shared";
2
+ import { FiLock, FiUnlock } from "react-icons/fi";
3
+ import type { SurfaceId } from "./app-model";
4
+ import { text } from "./app-model";
5
+
6
+ export function vaultCollectionForSurface(
7
+ vault: GuiVaultStatusData,
8
+ surface: SurfaceId,
9
+ ): GuiVaultCollectionSummary | undefined {
10
+ return vault.collections.find((collection) => collection.surface_ids.includes(surface));
11
+ }
12
+
13
+ export function isVaultSurfaceLocked(vault: GuiVaultStatusData, surface: SurfaceId): boolean {
14
+ const collection = vaultCollectionForSurface(vault, surface);
15
+ return surface !== "vault" && collection?.encrypted === true && !vault.unlocked;
16
+ }
17
+
18
+ export function VaultPadlock({ collection, compact = false, vault }: {
19
+ collection: GuiVaultCollectionSummary;
20
+ compact?: boolean;
21
+ vault: GuiVaultStatusData;
22
+ }) {
23
+ const locked = collection.state !== "unlocked" || !vault.unlocked;
24
+ const stateLabel = locked ? "Locked" : "Unlocked";
25
+ const Icon = locked ? FiLock : FiUnlock;
26
+ const tooltip = `${stateLabel}: ${text.vaultTooltip}`;
27
+
28
+ return (
29
+ <span
30
+ aria-label={`${stateLabel} by aidevops Vault: ${collection.label}`}
31
+ className={compact ? "vault-padlock compact" : "vault-padlock"}
32
+ data-vault-state={locked ? "locked" : "unlocked"}
33
+ title={tooltip}
34
+ >
35
+ <Icon aria-hidden="true" focusable="false" />
36
+ <span>{compact ? stateLabel : `${stateLabel} by Vault`}</span>
37
+ </span>
38
+ );
39
+ }
@@ -32,6 +32,7 @@ export const surfaceIconNames = {
32
32
  hardDrive: true,
33
33
  link: true,
34
34
  list: true,
35
+ lock: true,
35
36
  mail: true,
36
37
  message: true,
37
38
  note: true,
@@ -45,6 +46,7 @@ export const surfaceIconNames = {
45
46
  export type SurfaceIconName = keyof typeof surfaceIconNames;
46
47
  export const surfaceIds = [
47
48
  "overview",
49
+ "vault",
48
50
  "agents",
49
51
  "config",
50
52
  "localSetup",
@@ -180,7 +182,7 @@ export const text = {
180
182
  aiApps: "AI Apps",
181
183
  aiAppsIntro: "Local AI app, CLI, config, and aidevops prompt targets. Versions are read-only and compare installed aidevops files with the current checkout.",
182
184
  aiProviders: "AI Providers",
183
- aiProvidersIntro: "OAuth pool account metadata by provider. Tokens stay hidden; management controls are shown as planned safe actions.",
185
+ aiProvidersIntro: "OAuth pool account metadata by provider. Tokens stay hidden; Provider AI means task data leaves this device unless Vault policy routes it to Local AI or Hybrid mode.",
184
186
  madeForCreators: "Made for creators",
185
187
  maintenance: "Maintenance",
186
188
  management: "Management",
@@ -218,7 +220,7 @@ export const text = {
218
220
  searchPlaceholder: "Search setup, files, providers",
219
221
  secrets: "Secrets",
220
222
  security: "Secrets",
221
- securityBoundary: "No write routes, no shell bridge, no hosted app control, no pairing, no secret values.",
223
+ securityBoundary: "No write routes, no shell bridge, no hosted app control, no pairing, no secret values. Vault policy distinguishes Provider AI, Local AI, and Hybrid modes before protected data leaves the device.",
222
224
  servers: "Servers",
223
225
  serversIntro: "Server inventory by provider, operating system, orchestrator, and installed apps.",
224
226
  setup: "Setup",
@@ -238,6 +240,21 @@ export const text = {
238
240
  fontSize: "Font size",
239
241
  truncated: "Preview truncated for safety.",
240
242
  update: "Update",
243
+ vault: "Vault",
244
+ vaultAudit: "Audit Logs",
245
+ vaultBackups: "Backups & Recovery",
246
+ vaultCollectionIntro: "Encrypted collections are metadata-only while locked. Protected previews and write actions stay disabled until a local unlock succeeds.",
247
+ vaultDevices: "Devices",
248
+ vaultIntro: "Local encrypted metadata, lock/unlock readiness, device trust, encrypted sync, secure messages, backups, and audit placeholders.",
249
+ vaultLockedPreview: "Vault locked: protected previews are hidden and write actions are disabled.",
250
+ vaultLockUnlock: "Lock / Unlock",
251
+ vaultMessages: "Secure Messages",
252
+ vaultSetup: "First-use Setup",
253
+ vaultStatus: "Status",
254
+ vaultSync: "Sync",
255
+ vaultTooltip: "Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.",
256
+ vaultUnlockCta: "Unlock Vault",
257
+ vaultUnlockHint: "Run `aidevops vault unlock` in a local terminal and enter the passphrase only into the hidden prompt.",
241
258
  website: "Website",
242
259
  websites: "Websites",
243
260
  websitesIntro: "Websites, landing pages, publishing homes, and ownership status are planned.",
@@ -286,6 +303,7 @@ export const navGroups: SurfaceNavGroup[] = [
286
303
  label: text.operations,
287
304
  mode: "devops",
288
305
  items: [
306
+ { id: "vault", label: text.vault, description: "Encrypted metadata, setup, sync, and audit", icon: "lock" },
289
307
  { id: "agents", label: "Agents", description: "~/.aidevops/agents explorer", icon: "document" },
290
308
  { id: "config", label: text.config, description: "~/.config/aidevops explorer", icon: "settings" },
291
309
  { id: "localSetup", label: text.localSetup, description: "~/.aidevops explorer", icon: "terminal" },
@@ -392,6 +410,7 @@ export const installationRows = [
392
410
 
393
411
  export const plannedHomes = [
394
412
  { area: "Setup/status", home: "Dashboard + Local Setup", phase: "P6" },
413
+ { area: "Vault", home: "Vault status, setup, devices, sync, messages, backups, audit", phase: "P6/Vault" },
395
414
  { area: "Local and remote repos", home: "Local Repos + Remote Repos", phase: "P7" },
396
415
  { area: "Infrastructure inventory", home: "Domains, Registrars, Hosts, Servers", phase: "P8" },
397
416
  { area: "Provider catalog", home: "Registrars, Hosts, Apps", phase: "P9" },
@@ -536,6 +555,7 @@ export function surfaceRecordCounts(status: GuiStatusData): SurfaceRecordCounts
536
555
  registrars: populatedInventoryRowCount("registrars"),
537
556
  security: status.secrets.length,
538
557
  servers: populatedInventoryRowCount("servers"),
558
+ vault: status.vault.collections.length,
539
559
  };
540
560
  }
541
561
 
@@ -27,6 +27,9 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
27
27
  const aiApps = status.data.ai_apps
28
28
  .map((app) => `<li>${escapeHtml(app.name)}: ${escapeHtml(app.status)} ${escapeHtml(app.aidevops_version)} → ${escapeHtml(app.latest_version)}</li>`)
29
29
  .join("");
30
+ const vaultCollections = status.data.vault.collections
31
+ .map((collection) => `<li>${escapeHtml(collection.label)}: ${escapeHtml(collection.state)} ${escapeHtml(collection.preview_policy)}</li>`)
32
+ .join("");
30
33
  const settings = status.data.settings.keys
31
34
  .map((key) => `<li>${escapeHtml(key)}</li>`)
32
35
  .join("");
@@ -35,7 +38,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
35
38
  .join("");
36
39
  const navSections = [
37
40
  { heading: "Development", items: ["Local Repos", "Remote Repos", "Secrets", "AI Providers"] },
38
- { heading: "Operations", items: ["Dashboard", "Agents file explorer", "Config", "Local Setup", "Routines"] },
41
+ { heading: "Operations", items: ["Dashboard", "Vault", "Agents file explorer", "Config", "Local Setup", "Routines"] },
39
42
  { heading: "Infrastructure", items: ["Devices", "VPNs & Proxies", "Apps", "Installation", "Registrars", "Hosts", "Servers"] },
40
43
  { heading: "Identities", items: ["Brands", "Domains", "Personas"] },
41
44
  { heading: "Sites", items: ["Websites", "Forums", "Social Media", "Marketplaces"] },
@@ -53,11 +56,13 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
53
56
  `<p>AI-assisted development workflows, code quality, and deployment automation.</p>`,
54
57
  `<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Menlo (default), Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
55
58
  `<p>Secret references stay read-only and redacted.</p>`,
59
+ `<p>Vault surfaces show padlock indicators. Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.</p>`,
56
60
  `<p>${escapeHtml(status.data.update.message)}</p>`,
57
61
  renderedNavSections,
58
62
  `<h2>Path health</h2><ul>${paths}</ul>`,
59
63
  `<h2>Installed aidevops targets</h2><ul>${setupTargets}</ul>`,
60
64
  `<h2>AI Apps</h2><ul>${aiApps}</ul>`,
65
+ `<h2>Vault</h2><p>${escapeHtml(status.data.vault.status)} / ${escapeHtml(status.data.vault.setup_state)}</p><ul>${vaultCollections}</ul>`,
61
66
  `<h2>Local Repo Setup</h2><ul>${localRepos}</ul>`,
62
67
  `<h2>Remote Repos</h2><ul>${repos}</ul>`,
63
68
  `<h2>AI Provider Pools</h2><ul>${aiProviders}</ul>`,
@@ -85,6 +85,17 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
85
85
  oauth_pool: { ...statusFixture.oauth_pool, ...data.oauth_pool, providers: data.oauth_pool?.providers ?? statusFixture.oauth_pool.providers },
86
86
  setup_targets: data.setup_targets ?? statusFixture.setup_targets,
87
87
  ai_apps: data.ai_apps ?? statusFixture.ai_apps,
88
+ vault: {
89
+ ...statusFixture.vault,
90
+ ...data.vault,
91
+ readiness: { ...statusFixture.vault.readiness, ...data.vault?.readiness },
92
+ collections: data.vault?.collections ?? statusFixture.vault.collections,
93
+ devices: data.vault?.devices ?? statusFixture.vault.devices,
94
+ sync: { ...statusFixture.vault.sync, ...data.vault?.sync },
95
+ secure_messages: { ...statusFixture.vault.secure_messages, ...data.vault?.secure_messages },
96
+ backups: { ...statusFixture.vault.backups, ...data.vault?.backups },
97
+ audit: { ...statusFixture.vault.audit, ...data.vault?.audit },
98
+ },
88
99
  capabilities: data.capabilities ?? statusFixture.capabilities,
89
100
  secrets: data.secrets ?? statusFixture.secrets,
90
101
  placeholders: data.placeholders ?? statusFixture.placeholders,
@@ -430,6 +430,39 @@ code {
430
430
  font-weight: 700;
431
431
  }
432
432
 
433
+ .vault-padlock {
434
+ align-items: center;
435
+ background: color-mix(in srgb, #d97706 13%, transparent);
436
+ border: 1px solid color-mix(in srgb, #d97706 38%, transparent);
437
+ border-radius: 999px;
438
+ color: #b45309;
439
+ display: inline-flex;
440
+ flex: 0 0 auto;
441
+ font-size: 0.74rem;
442
+ font-weight: 900;
443
+ gap: 4px;
444
+ padding: 5px 9px;
445
+ text-transform: uppercase;
446
+ white-space: nowrap;
447
+ }
448
+
449
+ .vault-padlock svg {
450
+ height: 14px;
451
+ stroke-width: 2.6;
452
+ width: 14px;
453
+ }
454
+
455
+ .vault-padlock.compact {
456
+ font-size: 0.66rem;
457
+ padding: 3px 6px;
458
+ }
459
+
460
+ .vault-padlock[data-vault-state="unlocked"] {
461
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
462
+ border-color: color-mix(in srgb, var(--accent) 38%, transparent);
463
+ color: var(--accent);
464
+ }
465
+
433
466
  .sidebar-group h2 {
434
467
  font-size: max(12.5px, 0.84em);
435
468
  }
@@ -755,6 +788,32 @@ code {
755
788
  padding: clamp(24px, 5vw, 42px);
756
789
  }
757
790
 
791
+ .vault-hero {
792
+ display: grid;
793
+ gap: 16px;
794
+ }
795
+
796
+ .vault-readiness-strip {
797
+ display: flex;
798
+ flex-wrap: wrap;
799
+ gap: 8px;
800
+ }
801
+
802
+ .vault-readiness-strip span {
803
+ background: var(--surface-elevated);
804
+ border: 1px solid var(--border);
805
+ border-radius: 999px;
806
+ color: var(--text-secondary);
807
+ display: inline-flex;
808
+ gap: 6px;
809
+ padding: 7px 10px;
810
+ }
811
+
812
+ .vault-readiness-strip strong {
813
+ color: var(--accent);
814
+ text-transform: uppercase;
815
+ }
816
+
758
817
  .compact-hero h2 {
759
818
  font-size: clamp(3rem, 10vw, 5rem);
760
819
  letter-spacing: -0.08em;
@@ -1141,7 +1200,9 @@ code {
1141
1200
  .repo-card-grid,
1142
1201
  .provider-grid,
1143
1202
  .setup-card-grid,
1144
- .ai-app-grid {
1203
+ .ai-app-grid,
1204
+ .vault-card-grid,
1205
+ .vault-device-grid {
1145
1206
  display: grid;
1146
1207
  gap: 14px;
1147
1208
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@@ -1150,7 +1211,9 @@ code {
1150
1211
  .repo-setup-card,
1151
1212
  .provider-card,
1152
1213
  .setup-target-card,
1153
- .ai-app-card {
1214
+ .ai-app-card,
1215
+ .vault-feature-card,
1216
+ .vault-device-card {
1154
1217
  background: var(--surface-muted);
1155
1218
  border: 1px solid var(--border);
1156
1219
  border-radius: 18px;
@@ -1175,11 +1238,48 @@ code {
1175
1238
  .repo-setup-card h3,
1176
1239
  .provider-card h3,
1177
1240
  .setup-target-card h3,
1178
- .ai-app-card h3 {
1241
+ .ai-app-card h3,
1242
+ .vault-device-card h3 {
1179
1243
  font-size: 1rem;
1180
1244
  margin: 0 0 5px;
1181
1245
  }
1182
1246
 
1247
+ .vault-feature-card span {
1248
+ color: var(--accent);
1249
+ font-size: 0.76rem;
1250
+ font-weight: 900;
1251
+ text-transform: uppercase;
1252
+ }
1253
+
1254
+ .vault-feature-card strong {
1255
+ display: block;
1256
+ font-size: 1.35rem;
1257
+ letter-spacing: -0.04em;
1258
+ }
1259
+
1260
+ .vault-feature-card small {
1261
+ color: var(--text-muted);
1262
+ }
1263
+
1264
+ .vault-step-list {
1265
+ color: var(--text-secondary);
1266
+ margin: 0;
1267
+ padding-left: 1.2rem;
1268
+ }
1269
+
1270
+ .vault-setup-panel code {
1271
+ display: inline-block;
1272
+ margin-top: 10px;
1273
+ }
1274
+
1275
+ .vault-cta {
1276
+ justify-self: start;
1277
+ }
1278
+
1279
+ .vault-collection-list li {
1280
+ align-items: center;
1281
+ }
1282
+
1183
1283
  .repo-setup-card code,
1184
1284
  .repo-remotes code,
1185
1285
  .setup-path-row code {
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.22.3
15
+ # Version: 3.22.5
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)