factory-ai 1.2.1 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,14 @@ All notable changes follow semantic versioning and the Keep a Changelog structur
11
11
  - Azure and Bedrock provider wizard.
12
12
  - Durable evaluation, analytics, scaling, policy, extension, and recovery roadmap.
13
13
 
14
+ ## [1.3.0] - 2026-07-13
15
+
16
+ ### Added
17
+
18
+ - RBAC-protected Azure Blob dashboard snapshots for lock-free multi-operator TUI access.
19
+ - One-minute snapshot timer and direct local Azure-identity reads.
20
+ - Factory AI process, container, journal, Azure, Bedrock, MCP, Git, and PR identity metadata.
21
+
14
22
  ## [1.2.1] - 2026-07-13
15
23
 
16
24
  ### Fixed
package/bin/factory CHANGED
@@ -9,6 +9,7 @@ RESOURCE_GROUP=${FACTORY_RESOURCE_GROUP:-factory-ai-rg}
9
9
  VM=${FACTORY_VM:-agent-factory-vm}
10
10
  NAMESPACE=${FACTORY_SERVICE_BUS:-}
11
11
  VAULT=${FACTORY_KEY_VAULT:-}
12
+ STORAGE=${FACTORY_STORAGE_ACCOUNT:-}
12
13
 
13
14
  remote() {
14
15
  az vm run-command invoke --resource-group "$RESOURCE_GROUP" --name "$VM" \
@@ -73,7 +74,7 @@ case "$command" in
73
74
  exec "$0" submit "$repo" "$objective"
74
75
  ;;
75
76
  ui)
76
- FACTORY_RESOURCE_GROUP="$RESOURCE_GROUP" FACTORY_VM="$VM" FACTORY_SERVICE_BUS="$NAMESPACE" FACTORY_KEY_VAULT="$VAULT" node "$FACTORY_ROOT/src/tui.js"
77
+ FACTORY_RESOURCE_GROUP="$RESOURCE_GROUP" FACTORY_VM="$VM" FACTORY_SERVICE_BUS="$NAMESPACE" FACTORY_KEY_VAULT="$VAULT" FACTORY_STORAGE_ACCOUNT="$STORAGE" node "$FACTORY_ROOT/src/tui.js"
77
78
  ;;
78
79
  setup)
79
80
  for dependency in az gh jq curl ssh-keygen node; do command -v "$dependency" >/dev/null || { printf 'Missing dependency: %s\n' "$dependency" >&2; exit 1; }; done
@@ -96,6 +97,7 @@ case "$command" in
96
97
  deployment=$(az deployment group create --name agent-factory-setup --resource-group "$RESOURCE_GROUP" --template-file "$FACTORY_ROOT/infra/main.bicep" --parameters adminSshKey="$(< "$key.pub")" operatorObjectId="$operator" --output json)
97
98
  vault=$(jq -r '.properties.outputs.keyVaultName.value' <<<"$deployment")
98
99
  namespace=$(jq -r '.properties.outputs.serviceBusNamespace.value' <<<"$deployment")
100
+ storage=$(jq -r '.properties.outputs.storageAccount.value' <<<"$deployment")
99
101
  VAULT=$vault
100
102
  NAMESPACE=$namespace
101
103
  mkdir -p "$(dirname "$CONFIG_FILE")"
@@ -104,6 +106,7 @@ case "$command" in
104
106
  printf 'FACTORY_VM=%q\n' agent-factory-vm
105
107
  printf 'FACTORY_SERVICE_BUS=%q\n' "$namespace"
106
108
  printf 'FACTORY_KEY_VAULT=%q\n' "$vault"
109
+ printf 'FACTORY_STORAGE_ACCOUNT=%q\n' "$storage"
107
110
  } > "$CONFIG_FILE"
108
111
  chmod 0600 "$CONFIG_FILE"
109
112
  ip=$(curl -fsS https://api.ipify.org)
@@ -151,7 +154,7 @@ case "$command" in
151
154
  printf 'Azure foundation and credential vault ready. Config: %s\n' "$CONFIG_FILE"
152
155
  if [[ $deploy_now == true ]]; then
153
156
  source_ref=$(gh api repos/itsvedantkumar/factory-ai/commits/main --jq .sha)
154
- parameters=(KEY_VAULT_NAME "$vault" SERVICE_BUS_NAMESPACE "$namespace" SERVICE_BUS_QUEUE code-tasks SOURCE_REPOSITORY itsvedantkumar/factory-ai SOURCE_REF "$source_ref")
157
+ parameters=(KEY_VAULT_NAME "$vault" SERVICE_BUS_NAMESPACE "$namespace" SERVICE_BUS_QUEUE code-tasks FACTORY_STORAGE_ACCOUNT "$storage" SOURCE_REPOSITORY itsvedantkumar/factory-ai SOURCE_REF "$source_ref")
155
158
  if [[ $provider == bedrock ]]; then
156
159
  parameters+=(AWS_REGION "$aws_region")
157
160
  for role in SCOUT PLANNER BUILDER TESTER DEBUGGER REVIEWER SECURITY RELEASE; do parameters+=("FACTORY_MODEL_$role" "bedrock/$bedrock_builder"); done
@@ -7,6 +7,7 @@ StartLimitBurst=10
7
7
 
8
8
  [Service]
9
9
  Type=simple
10
+ SyslogIdentifier=factory-ai-control
10
11
  User=factory
11
12
  Group=factory
12
13
  WorkingDirectory=/opt/agent-factory/app
@@ -5,6 +5,7 @@ Wants=network-online.target
5
5
 
6
6
  [Service]
7
7
  Type=simple
8
+ SyslogIdentifier=factory-ai-release
8
9
  User=factory
9
10
  Group=factory
10
11
  WorkingDirectory=/opt/agent-factory/app
@@ -4,6 +4,7 @@ After=agent-factory-worker.service
4
4
 
5
5
  [Service]
6
6
  Type=oneshot
7
+ SyslogIdentifier=factory-ai-reporter
7
8
  User=factory
8
9
  Group=factory
9
10
  WorkingDirectory=/opt/agent-factory/app
@@ -5,6 +5,7 @@ Wants=network-online.target
5
5
 
6
6
  [Service]
7
7
  Type=simple
8
+ SyslogIdentifier=factory-ai-telegram
8
9
  User=factory
9
10
  Group=factory
10
11
  WorkingDirectory=/opt/agent-factory/app
@@ -8,6 +8,7 @@ StartLimitBurst=10
8
8
 
9
9
  [Service]
10
10
  Type=simple
11
+ SyslogIdentifier=factory-ai-worker
11
12
  User=factory
12
13
  Group=factory
13
14
  WorkingDirectory=/opt/agent-factory/app
@@ -20,7 +20,7 @@ while (($# > 0)); do
20
20
  shift
21
21
  fi
22
22
  case "$name" in
23
- KEY_VAULT_NAME|SERVICE_BUS_NAMESPACE|SERVICE_BUS_QUEUE|SOURCE_REPOSITORY|SOURCE_REF|AWS_REGION|FACTORY_MODEL_SCOUT|FACTORY_MODEL_PLANNER|FACTORY_MODEL_BUILDER|FACTORY_MODEL_TESTER|FACTORY_MODEL_DEBUGGER|FACTORY_MODEL_REVIEWER|FACTORY_MODEL_SECURITY|FACTORY_MODEL_RELEASE) printf -v "$name" '%s' "$value" ;;
23
+ KEY_VAULT_NAME|SERVICE_BUS_NAMESPACE|SERVICE_BUS_QUEUE|SOURCE_REPOSITORY|SOURCE_REF|FACTORY_STORAGE_ACCOUNT|AWS_REGION|FACTORY_MODEL_SCOUT|FACTORY_MODEL_PLANNER|FACTORY_MODEL_BUILDER|FACTORY_MODEL_TESTER|FACTORY_MODEL_DEBUGGER|FACTORY_MODEL_REVIEWER|FACTORY_MODEL_SECURITY|FACTORY_MODEL_RELEASE) printf -v "$name" '%s' "$value" ;;
24
24
  *) echo "Unknown deployment parameter: $name" >&2; exit 1 ;;
25
25
  esac
26
26
  done
@@ -62,6 +62,7 @@ KEY_VAULT_NAME="$KEY_VAULT_NAME" \
62
62
  SERVICE_BUS_NAMESPACE="$SERVICE_BUS_NAMESPACE" \
63
63
  SERVICE_BUS_QUEUE="$SERVICE_BUS_QUEUE" \
64
64
  FACTORY_WORKER_IMAGE="agent-factory-worker:$SOURCE_REF" \
65
+ FACTORY_STORAGE_ACCOUNT="${FACTORY_STORAGE_ACCOUNT:-}" \
65
66
  AWS_REGION="${AWS_REGION:-}" \
66
67
  FACTORY_MODEL_SCOUT="${FACTORY_MODEL_SCOUT:-}" \
67
68
  FACTORY_MODEL_PLANNER="${FACTORY_MODEL_PLANNER:-}" \
@@ -0,0 +1,18 @@
1
+ [Unit]
2
+ Description=Publish Factory AI operator dashboard snapshot
3
+ After=network-online.target agent-factory-control.service
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=oneshot
8
+ User=factory
9
+ Group=factory
10
+ WorkingDirectory=/opt/agent-factory/app
11
+ EnvironmentFile=/etc/agent-factory-control.env
12
+ ExecStart=/usr/bin/node /opt/agent-factory/app/src/snapshot.js
13
+ NoNewPrivileges=true
14
+ PrivateTmp=true
15
+ ProtectSystem=strict
16
+ ProtectHome=true
17
+ UMask=0027
18
+ SyslogIdentifier=factory-ai-snapshot
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Refresh Factory AI operator dashboard every minute
3
+
4
+ [Timer]
5
+ OnBootSec=2m
6
+ OnUnitActiveSec=1m
7
+ RandomizedDelaySec=5
8
+ Persistent=true
9
+
10
+ [Install]
11
+ WantedBy=timers.target
@@ -6,6 +6,7 @@ Requires=docker.service
6
6
 
7
7
  [Service]
8
8
  Type=oneshot
9
+ SyslogIdentifier=factory-ai-updater
9
10
  User=root
10
11
  Group=root
11
12
  ExecStart=/opt/agent-factory/app/bootstrap/auto-update.sh
@@ -35,6 +35,7 @@ test "$(node --version | cut -d. -f1)" = "v20"
35
35
  id "$FACTORY_USER" >/dev/null 2>&1 || useradd --system --create-home --shell /usr/sbin/nologin "$FACTORY_USER"
36
36
  test -f "$APP_DIR/package-lock.json"
37
37
  npm ci --omit=dev --prefix "$APP_DIR"
38
+ factory_version=$(node -p "require('$APP_DIR/package.json').version")
38
39
  docker build --file "$APP_DIR/Dockerfile.worker" --tag "$FACTORY_WORKER_IMAGE" "$APP_DIR"
39
40
  for scanner_image in \
40
41
  aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \
@@ -74,13 +75,16 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory.env
74
75
  printf 'KEY_VAULT_NAME=%s\n' "$KEY_VAULT_NAME"
75
76
  printf 'AZURE_SUBSCRIPTION_ID=%s\n' "$subscription_id"
76
77
  printf 'FACTORY_RESOURCE_GROUP=%s\n' "$resource_group"
78
+ [[ -n ${FACTORY_STORAGE_ACCOUNT:-} ]] && printf 'FACTORY_STORAGE_ACCOUNT=%s\n' "$FACTORY_STORAGE_ACCOUNT"
77
79
  printf 'FACTORY_STATE_DIR=/opt/agent-factory/state\n'
78
80
  printf 'FACTORY_WORKSPACE_DIR=/opt/agent-factory/workspaces\n'
79
81
  printf 'FACTORY_REGISTRY=%s/config/capabilities.json\n' "$APP_DIR"
80
82
  printf 'FACTORY_WORKER_IMAGE=%s\n' "$FACTORY_WORKER_IMAGE"
83
+ printf 'FACTORY_VERSION=%s\n' "$factory_version"
81
84
  printf 'MAX_CONCURRENCY=3\n'
82
85
  printf 'TASK_TIMEOUT_MS=1800000\n'
83
86
  printf 'MAX_DELIVERY_COUNT=8\n'
87
+ printf 'FACTORY_VERSION=%s\n' "$factory_version"
84
88
  [[ -n ${AWS_REGION:-} ]] && printf 'AWS_REGION=%s\nAWS_DEFAULT_REGION=%s\n' "$AWS_REGION" "$AWS_REGION"
85
89
  for variable in FACTORY_MODEL_SCOUT FACTORY_MODEL_PLANNER FACTORY_MODEL_BUILDER FACTORY_MODEL_TESTER FACTORY_MODEL_DEBUGGER FACTORY_MODEL_REVIEWER FACTORY_MODEL_SECURITY FACTORY_MODEL_RELEASE; do
86
90
  [[ -n ${!variable:-} ]] && printf '%s=%s\n' "$variable" "${!variable}"
@@ -96,6 +100,7 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory-control.env
96
100
  printf 'KEY_VAULT_NAME=%s\n' "$KEY_VAULT_NAME"
97
101
  printf 'AZURE_SUBSCRIPTION_ID=%s\n' "$subscription_id"
98
102
  printf 'FACTORY_RESOURCE_GROUP=%s\n' "$resource_group"
103
+ [[ -n ${FACTORY_STORAGE_ACCOUNT:-} ]] && printf 'FACTORY_STORAGE_ACCOUNT=%s\n' "$FACTORY_STORAGE_ACCOUNT"
99
104
  printf 'FACTORY_STATE_DIR=/opt/agent-factory/state\n'
100
105
  printf 'FACTORY_REGISTRY=%s/config/capabilities.json\n' "$APP_DIR"
101
106
  printf 'MAX_DELIVERY_COUNT=8\n'
@@ -117,6 +122,8 @@ install -m 0644 "$APP_DIR/bootstrap/agent-factory-telegram.service" /etc/systemd
117
122
  chmod 0755 "$APP_DIR/bootstrap/auto-update.sh"
118
123
  install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.service" /etc/systemd/system/factory-ai-update.service
119
124
  install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.timer" /etc/systemd/system/factory-ai-update.timer
125
+ install -m 0644 "$APP_DIR/bootstrap/factory-ai-snapshot.service" /etc/systemd/system/factory-ai-snapshot.service
126
+ install -m 0644 "$APP_DIR/bootstrap/factory-ai-snapshot.timer" /etc/systemd/system/factory-ai-snapshot.timer
120
127
  systemctl daemon-reload
121
128
  systemctl enable --now agent-factory-worker.service
122
129
  systemctl enable --now agent-factory-control.service
@@ -124,5 +131,6 @@ systemctl enable --now agent-factory-release.service
124
131
  systemctl enable --now agent-factory-reporter.timer
125
132
  systemctl enable --now agent-factory-telegram.service
126
133
  systemctl enable --now factory-ai-update.timer
134
+ systemctl enable --now factory-ai-snapshot.timer
127
135
  systemctl restart agent-factory-control.service agent-factory-worker.service agent-factory-release.service
128
136
  echo "Agent factory worker installed"
package/infra/main.bicep CHANGED
@@ -20,6 +20,7 @@ var prefix = 'agent-factory'
20
20
  var unique = uniqueString(subscription().subscriptionId, resourceGroup().id)
21
21
  var keyVaultName = 'af${take(unique, 18)}'
22
22
  var serviceBusName = 'af-${unique}'
23
+ var storageName = 'fa${take(unique, 20)}'
23
24
 
24
25
  resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
25
26
  name: '${prefix}-nsg'
@@ -147,6 +148,43 @@ resource vault 'Microsoft.KeyVault/vaults@2024-11-01' = {
147
148
  }
148
149
  }
149
150
 
151
+ resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = {
152
+ name: storageName
153
+ location: location
154
+ sku: {
155
+ name: 'Standard_ZRS'
156
+ }
157
+ kind: 'StorageV2'
158
+ properties: {
159
+ allowBlobPublicAccess: false
160
+ allowSharedKeyAccess: false
161
+ defaultToOAuthAuthentication: true
162
+ minimumTlsVersion: 'TLS1_2'
163
+ publicNetworkAccess: 'Enabled'
164
+ supportsHttpsTrafficOnly: true
165
+ }
166
+ }
167
+
168
+ resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-01-01' = {
169
+ parent: storage
170
+ name: 'default'
171
+ properties: {
172
+ deleteRetentionPolicy: {
173
+ enabled: true
174
+ days: 30
175
+ }
176
+ isVersioningEnabled: true
177
+ }
178
+ }
179
+
180
+ resource operatorContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = {
181
+ parent: blobService
182
+ name: 'operator'
183
+ properties: {
184
+ publicAccess: 'None'
185
+ }
186
+ }
187
+
150
188
  resource serviceBus 'Microsoft.ServiceBus/namespaces@2024-01-01' = {
151
189
  name: serviceBusName
152
190
  location: location
@@ -299,11 +337,32 @@ resource workerCostReader 'Microsoft.Authorization/roleAssignments@2022-04-01' =
299
337
  }
300
338
  }
301
339
 
340
+ resource workerBlobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
341
+ name: guid(storage.id, vm.id, 'storage-blob-data-contributor')
342
+ scope: storage
343
+ properties: {
344
+ principalId: vm.identity.principalId
345
+ principalType: 'ServicePrincipal'
346
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
347
+ }
348
+ }
349
+
350
+ resource operatorBlobReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
351
+ name: guid(storage.id, operatorObjectId, 'storage-blob-data-reader')
352
+ scope: storage
353
+ properties: {
354
+ principalId: operatorObjectId
355
+ principalType: 'User'
356
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')
357
+ }
358
+ }
359
+
302
360
  output vmName string = vm.name
303
361
  output privateIp string = nic.properties.ipConfigurations[0].properties.privateIPAddress
304
362
  output egressIp string = egressIp.properties.ipAddress
305
363
  output keyVaultName string = vault.name
306
364
  output serviceBusNamespace string = serviceBus.name
365
+ output storageAccount string = storage.name
307
366
  output controlQueue string = queues[0].name
308
367
  output agentQueue string = queues[1].name
309
368
  output releaseQueue string = queues[2].name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factory-ai",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Deploy a private autonomous coding-agent factory on Azure: isolated builders, testers, security reviewers, durable orchestration, multi-model routing, memory, cost controls, and gated GitHub pull requests.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -40,6 +40,7 @@
40
40
  "agent-factory-release": "src/release-service.js",
41
41
  "agent-factory-task": "src/task-entry.js",
42
42
  "factory-ai-telegram": "src/telegram-service.js",
43
+ "factory-ai-snapshot": "src/snapshot.js",
43
44
  "agent-factory-worker": "src/worker.js"
44
45
  },
45
46
  "files": [
@@ -63,6 +64,7 @@
63
64
  "@azure/identity": "4.13.1",
64
65
  "@azure/keyvault-secrets": "4.10.0",
65
66
  "@azure/service-bus": "7.9.5",
67
+ "@azure/storage-blob": "12.28.0",
66
68
  "@inquirer/prompts": "8.5.2",
67
69
  "@modelcontextprotocol/sdk": "1.29.0",
68
70
  "@modelcontextprotocol/server-memory": "2026.7.4",
@@ -31,6 +31,7 @@ export class AzureResponsesHarness {
31
31
  retries = 4,
32
32
  timeoutMs = 180_000,
33
33
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
34
+ userAgent = `factory-ai/${process.env.FACTORY_VERSION ?? "dev"}`,
34
35
  }) {
35
36
  this.endpoint = `${baseUrl.replace(/\/$/, "")}/responses`;
36
37
  this.apiKey = apiKey;
@@ -42,6 +43,7 @@ export class AzureResponsesHarness {
42
43
  this.retries = retries;
43
44
  this.timeoutMs = timeoutMs;
44
45
  this.sleep = sleep;
46
+ this.userAgent = userAgent;
45
47
  }
46
48
 
47
49
  async request(body) {
@@ -56,6 +58,8 @@ export class AzureResponsesHarness {
56
58
  "content-type": "application/json",
57
59
  "api-key": this.apiKey,
58
60
  authorization: `Bearer ${this.apiKey}`,
61
+ "user-agent": this.userAgent,
62
+ "x-ms-useragent": this.userAgent,
59
63
  },
60
64
  body: JSON.stringify(body),
61
65
  signal: controller.signal,
@@ -12,7 +12,7 @@ function definitions(tools) {
12
12
 
13
13
  export class BedrockHarness {
14
14
  constructor({ client, region = process.env.AWS_REGION ?? "us-east-1", model, tools, maxSteps = 40, maxOutputTokens = 4096 }) {
15
- this.client = client ?? new BedrockRuntimeClient({ region });
15
+ this.client = client ?? new BedrockRuntimeClient({ region, customUserAgent: `factory-ai/${process.env.FACTORY_VERSION ?? "dev"}` });
16
16
  this.model = model;
17
17
  this.tools = tools;
18
18
  this.maxSteps = maxSteps;
package/src/ceo.js CHANGED
@@ -6,6 +6,8 @@ import { loadConfig } from "./config.js";
6
6
  import { createBus, sendMessage } from "./bus.js";
7
7
  import { parseObjective } from "./validation.js";
8
8
 
9
+ process.title = "factory-ai-ceo";
10
+
9
11
  function argumentsFrom(argv) {
10
12
  const options = { baseBranch: "main", wait: false, timeoutMs: 3_600_000, words: [] };
11
13
  for (let index = 0; index < argv.length; index += 1) {
package/src/config.js CHANGED
@@ -9,6 +9,7 @@ const environmentSchema = z.object({
9
9
  KEY_VAULT_NAME: z.string().min(3),
10
10
  AZURE_SUBSCRIPTION_ID: z.string().optional(),
11
11
  FACTORY_RESOURCE_GROUP: z.string().default("factory-ai-rg"),
12
+ FACTORY_STORAGE_ACCOUNT: z.string().optional(),
12
13
  FACTORY_STATE_DIR: z.string().default("/opt/agent-factory/state"),
13
14
  FACTORY_WORKSPACE_DIR: z.string().default("/opt/agent-factory/workspaces"),
14
15
  FACTORY_REGISTRY: z.string().default("/opt/agent-factory/app/config/capabilities.json"),
@@ -41,6 +42,7 @@ export function loadConfig(environment = process.env) {
41
42
  keyVaultUrl: `https://${env.KEY_VAULT_NAME}.vault.azure.net`,
42
43
  subscriptionId: env.AZURE_SUBSCRIPTION_ID,
43
44
  resourceGroup: env.FACTORY_RESOURCE_GROUP,
45
+ storageAccount: env.FACTORY_STORAGE_ACCOUNT,
44
46
  secretNames: {
45
47
  TEXTVED_AZURE_API_KEY: env.AZURE_PRIMARY_API_KEY_SECRET,
46
48
  TEXTVED_AZURE_BASE_URL: env.AZURE_PRIMARY_BASE_URL_SECRET,
@@ -19,6 +19,7 @@ const AZURE_ENVIRONMENT = [
19
19
  "FACTORY_MODEL_REVIEWER",
20
20
  "FACTORY_MODEL_SECURITY",
21
21
  "FACTORY_MODEL_RELEASE",
22
+ "FACTORY_VERSION",
22
23
  ];
23
24
 
24
25
  function parseOutput(stdout) {
@@ -37,7 +38,7 @@ export class ContainerAgentRunner {
37
38
  }
38
39
 
39
40
  async executePacket(packet, directory) {
40
- const name = `agent-${packet.objective.id}-${packet.task?.id ?? "planner"}`.toLowerCase().replaceAll(/[^a-z0-9_.-]/g, "-").slice(0, 63);
41
+ const name = `factory-ai-${packet.objective.id}-${packet.task?.id ?? "planner"}`.toLowerCase().replaceAll(/[^a-z0-9_.-]/g, "-").slice(0, 63);
41
42
  const args = [
42
43
  "run", "-i", "--rm", "--name", name,
43
44
  "--read-only",
@@ -7,6 +7,7 @@ import { loadRegistry } from "./registry.js";
7
7
  import { log } from "./log.js";
8
8
  import { ProjectMemory } from "./project-memory.js";
9
9
 
10
+ process.title = "factory-ai-control";
10
11
  const config = loadConfig();
11
12
  const bus = createBus(config, config.controlQueue, config.agentQueue);
12
13
  const releaseSender = bus.client.createSender(config.releaseQueue);
package/src/operator.js CHANGED
@@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { gunzipSync } from "node:zlib";
5
+ import { DefaultAzureCredential } from "@azure/identity";
6
+ import { BlobServiceClient } from "@azure/storage-blob";
5
7
  import { run } from "./process.js";
6
8
 
7
9
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -21,6 +23,7 @@ export function createOperator(environment = process.env) {
21
23
  const vm = environment.FACTORY_VM ?? "agent-factory-vm";
22
24
  const namespace = environment.FACTORY_SERVICE_BUS ?? "";
23
25
  const vault = environment.FACTORY_KEY_VAULT ?? "";
26
+ const storageAccount = environment.FACTORY_STORAGE_ACCOUNT ?? "";
24
27
  const remote = async (script) => {
25
28
  for (let attempt = 0; attempt < 6; attempt += 1) {
26
29
  try { return extractRunCommand(await command("az", ["vm", "run-command", "invoke", "--resource-group", resourceGroup, "--name", vm, "--command-id", "RunShellScript", "--scripts", script, "--query", "value[0].message", "--output", "tsv"])); }
@@ -41,6 +44,15 @@ export function createOperator(environment = process.env) {
41
44
  };
42
45
  return {
43
46
  dashboard: async () => {
47
+ if (storageAccount) {
48
+ try {
49
+ const service = new BlobServiceClient(`https://${storageAccount}.blob.core.windows.net`, new DefaultAzureCredential());
50
+ const value = await service.getContainerClient("operator").getBlockBlobClient("dashboard.json").downloadToBuffer();
51
+ return JSON.parse(value.toString("utf8"));
52
+ } catch (error) {
53
+ if (!["BlobNotFound", "ContainerNotFound"].includes(error.code)) throw error;
54
+ }
55
+ }
44
56
  const encoded = await remote("sudo -u factory env $(xargs < /etc/agent-factory-control.env) node /opt/agent-factory/app/src/dashboard.js --json | gzip -c | base64 -w0");
45
57
  return JSON.parse(gunzipSync(Buffer.from(encoded, "base64")).toString("utf8"));
46
58
  },
@@ -56,7 +68,7 @@ export function createOperator(environment = process.env) {
56
68
  return command(path.join(root, "bin/factory"), [action]);
57
69
  },
58
70
  capabilities: async () => JSON.parse(await readFile(path.join(root, "config/capabilities.json"), "utf8")),
59
- config: () => ({ resourceGroup, vm, namespace, vault, models: { scout: "GPT-5.4 nano", simpleBuilder: "Kimi K2.7-Code", builder: "GPT-5.5", tester: "GPT-5.4", critical: "GPT-5.6" } }),
71
+ config: () => ({ resourceGroup, vm, namespace, vault, storageAccount, models: { scout: "GPT-5.4 nano", simpleBuilder: "Kimi K2.7-Code", builder: "GPT-5.5", tester: "GPT-5.4", critical: "GPT-5.6" } }),
60
72
  listSecrets: async () => withVault(async () => JSON.parse(await command("az", ["keyvault", "secret", "list", "--vault-name", vault, "--query", "[].{name:name,updated:attributes.updated}", "--output", "json"]))),
61
73
  setSecret: async (name, value) => withVault(async () => {
62
74
  if (!/^[A-Za-z0-9-]{1,127}$/.test(name) || !value) throw new Error("Valid secret name and value are required");
@@ -7,6 +7,7 @@ import { ReleaseBot } from "./release-bot.js";
7
7
  import { GitHubRelease } from "./release.js";
8
8
  import { log } from "./log.js";
9
9
 
10
+ process.title = "factory-ai-release";
10
11
  const config = loadConfig();
11
12
  Object.assign(process.env, await loadRuntimeSecrets(config, undefined, ["GH_TOKEN"]));
12
13
  await run("gh", ["auth", "setup-git"], { timeoutMs: 60_000 });
package/src/reporter.js CHANGED
@@ -4,6 +4,10 @@ import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { aggregateDashboard, loadAzureCost, loadLocalState, loadQueueMetrics, stableStringify } from "./dashboard.js";
6
6
  import { loadConfig } from "./config.js";
7
+ import { DefaultAzureCredential } from "@azure/identity";
8
+ import { BlobServiceClient } from "@azure/storage-blob";
9
+
10
+ process.title = "factory-ai-reporter";
7
11
 
8
12
  function markdown(dashboard) {
9
13
  const objectives = Object.entries(dashboard.summary.objectives).map(([state, count]) => `${state}=${count}`).join(", ") || "none";
@@ -30,16 +34,31 @@ export async function writeHourlyReport(root, dashboard, { now = new Date(), ret
30
34
  return stem;
31
35
  }
32
36
 
37
+ export async function uploadDashboardSnapshot(config, dashboard, {
38
+ credential = new DefaultAzureCredential(),
39
+ createClient = (url, auth) => new BlobServiceClient(url, auth),
40
+ } = {}) {
41
+ if (!config.storageAccount) return false;
42
+ const service = createClient(`https://${config.storageAccount}.blob.core.windows.net`, credential);
43
+ const blob = service.getContainerClient("operator").getBlockBlobClient("dashboard.json");
44
+ await blob.uploadData(Buffer.from(stableStringify(dashboard)), {
45
+ blobHTTPHeaders: { blobContentType: "application/json; charset=utf-8", blobCacheControl: "no-store" },
46
+ });
47
+ return true;
48
+ }
49
+
33
50
  async function main() {
34
51
  const root = process.env.FACTORY_STATE_DIR ?? "/opt/agent-factory/state";
52
+ const config = loadConfig();
35
53
  const loaded = await loadLocalState(root);
36
- const queue = process.env.SERVICE_BUS_NAMESPACE ? await loadQueueMetrics(loadConfig()) : {};
54
+ const queue = process.env.SERVICE_BUS_NAMESPACE ? await loadQueueMetrics(config) : {};
37
55
  let cost = null;
38
56
  if (process.env.AZURE_SUBSCRIPTION_ID) {
39
- try { cost = await loadAzureCost(loadConfig()); } catch (error) { loaded.warnings.push(`Cost unavailable: ${error.message}`); }
57
+ try { cost = await loadAzureCost(config); } catch (error) { loaded.warnings.push(`Cost unavailable: ${error.message}`); }
40
58
  }
41
59
  const dashboard = aggregateDashboard({ ...loaded, queue, cost, runtime: { status: "running" } });
42
60
  const stem = await writeHourlyReport(root, dashboard);
61
+ await uploadDashboardSnapshot(config, dashboard);
43
62
  process.stdout.write(`${JSON.stringify({ event: "hourly_report", report: stem })}\n`);
44
63
  }
45
64
 
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { aggregateDashboard, loadAzureCost, loadLocalState, loadQueueMetrics } from "./dashboard.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { uploadDashboardSnapshot } from "./reporter.js";
5
+
6
+ process.title = "factory-ai-snapshot";
7
+ const config = loadConfig();
8
+ const root = process.env.FACTORY_STATE_DIR ?? "/opt/agent-factory/state";
9
+ const loaded = await loadLocalState(root);
10
+ const queue = await loadQueueMetrics(config);
11
+ let cost = null;
12
+ try { cost = await loadAzureCost(config); } catch (error) { loaded.warnings.push(`Cost unavailable: ${error.message}`); }
13
+ const dashboard = aggregateDashboard({ ...loaded, queue, cost, runtime: { status: "running" } });
14
+ await uploadDashboardSnapshot(config, dashboard);
15
+ process.stdout.write(`${JSON.stringify({ event: "dashboard_snapshot", generatedAt: dashboard.generatedAt })}\n`);
package/src/task-entry.js CHANGED
@@ -10,6 +10,7 @@ const input = await new Promise((resolve, reject) => {
10
10
  process.stdin.on("error", reject);
11
11
  });
12
12
  const packet = JSON.parse(input);
13
+ process.title = `factory-ai-${packet.task?.role ?? packet.mode}`.slice(0, 63);
13
14
  const registry = await loadRegistry("/opt/agent-factory/app/config/capabilities.json");
14
15
  const runner = new AzureAgentRunner({ timeoutMs: 1_800_000 }, registry);
15
16
  let result;
@@ -11,6 +11,7 @@ import { loadLocalState, loadQueueMetrics } from "./dashboard.js";
11
11
  import { formatObjectiveProgress, isAllowedChat, objectiveFromTelegram, parseTelegramCommand } from "./telegram.js";
12
12
  import { log } from "./log.js";
13
13
 
14
+ process.title = "factory-ai-telegram";
14
15
  const config = loadConfig();
15
16
  Object.assign(process.env, await loadRuntimeSecrets(config, undefined, ["TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_CHAT_IDS"]));
16
17
  const token = process.env.TELEGRAM_BOT_TOKEN;
package/src/tui.js CHANGED
@@ -65,7 +65,7 @@ function renderCapabilities() {
65
65
 
66
66
  function renderSettings() {
67
67
  const config = operator.config();
68
- main.setContent(`{bold}Runtime{/}\n\n Resource group ${config.resourceGroup}\n VM ${config.vm}\n Service Bus ${config.namespace}\n Key Vault ${config.vault}\n\n{bold}Model policy{/}\n\n Scout GPT-5.4 nano\n Simple builder Kimi K2.7-Code\n Builder GPT-5.5\n Tester GPT-5.4\n Critical roles GPT-5.6\n\n{#7f8b99-fg}Run factory setup to change providers or role routes.{/}`);
68
+ main.setContent(`{bold}Runtime{/}\n\n Resource group ${config.resourceGroup}\n VM ${config.vm}\n Service Bus ${config.namespace}\n Key Vault ${config.vault}\n Operator state ${config.storageAccount || "Run Command fallback"}\n\n{bold}Model policy{/}\n\n Scout GPT-5.4 nano\n Simple builder Kimi K2.7-Code\n Builder GPT-5.5\n Tester GPT-5.4\n Critical roles GPT-5.6\n\n{#7f8b99-fg}Run factory setup to change providers or role routes.{/}`);
69
69
  }
70
70
 
71
71
  function render() {
package/src/updater.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
5
+ process.title = "factory-ai-updater";
6
+
5
7
  function parts(value) {
6
8
  if (!/^\d+\.\d+\.\d+$/.test(value)) throw new Error(`Unsupported version: ${value}`);
7
9
  return value.split(".").map(Number);
package/src/worker.js CHANGED
@@ -10,6 +10,7 @@ import { sendMessage } from "./bus.js";
10
10
  import { ContainerAgentRunner } from "./container-runner.js";
11
11
  import { ScannerSuite } from "./scanner-suite.js";
12
12
 
13
+ process.title = "factory-ai-worker";
13
14
  const config = loadConfig();
14
15
  Object.assign(process.env, await loadRuntimeSecrets(config));
15
16
  await run("gh", ["auth", "setup-git"], { timeoutMs: 60_000 });