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 +8 -0
- package/bin/factory +5 -2
- package/bootstrap/agent-factory-control.service +1 -0
- package/bootstrap/agent-factory-release.service +1 -0
- package/bootstrap/agent-factory-reporter.service +1 -0
- package/bootstrap/agent-factory-telegram.service +1 -0
- package/bootstrap/agent-factory-worker.service +1 -0
- package/bootstrap/deploy-runtime.sh +2 -1
- package/bootstrap/factory-ai-snapshot.service +18 -0
- package/bootstrap/factory-ai-snapshot.timer +11 -0
- package/bootstrap/factory-ai-update.service +1 -0
- package/bootstrap/setup.sh +8 -0
- package/infra/main.bicep +59 -0
- package/package.json +3 -1
- package/src/azure-harness.js +4 -0
- package/src/bedrock-harness.js +1 -1
- package/src/ceo.js +2 -0
- package/src/config.js +2 -0
- package/src/container-runner.js +2 -1
- package/src/control-service.js +1 -0
- package/src/operator.js +13 -1
- package/src/release-service.js +1 -0
- package/src/reporter.js +21 -2
- package/src/snapshot.js +15 -0
- package/src/task-entry.js +1 -0
- package/src/telegram-service.js +1 -0
- package/src/tui.js +1 -1
- package/src/updater.js +2 -0
- package/src/worker.js +1 -0
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
|
|
@@ -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
|
package/bootstrap/setup.sh
CHANGED
|
@@ -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.
|
|
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",
|
package/src/azure-harness.js
CHANGED
|
@@ -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,
|
package/src/bedrock-harness.js
CHANGED
|
@@ -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,
|
package/src/container-runner.js
CHANGED
|
@@ -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 = `
|
|
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",
|
package/src/control-service.js
CHANGED
|
@@ -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");
|
package/src/release-service.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
package/src/snapshot.js
ADDED
|
@@ -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;
|
package/src/telegram-service.js
CHANGED
|
@@ -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 });
|