@atmem/architect 0.2.0-alpha.4.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +21 -21
  4. package/cli/runtime.mjs +3 -1
  5. package/dist/assets/{index-qtCM2D7O.js → index-D4CZ_n8f.js} +19166 -19094
  6. package/dist/assets/index-DuveVTIx.css +1 -0
  7. package/dist/assets/{index.es-C6C0sneB.js → index.es-Fcqg-orY.js} +1 -1
  8. package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-4PDDTAP7.js} +3 -3
  9. package/dist/index.html +2 -2
  10. package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
  11. package/dist/licenses/memory-dependency-notices.txt +149 -2241
  12. package/package.json +3 -3
  13. package/public/licenses/atmem-APACHE-2.0.txt +202 -0
  14. package/public/licenses/memory-dependency-notices.txt +149 -2241
  15. package/server/app.py +1 -1
  16. package/server/codex_app_server.py +5 -5
  17. package/server/codex_worker_policy.py +27 -4
  18. package/server/engineering_artifacts.py +7 -1
  19. package/server/engineering_handoff.py +9 -0
  20. package/server/engineering_routes.py +16 -2
  21. package/server/local_providers.py +7 -3
  22. package/server/memory-requirements.lock +6 -6
  23. package/server/memory-requirements.txt +2 -2
  24. package/server/memory_context.py +58 -0
  25. package/server/memory_routes.py +67 -8
  26. package/server/memory_service.py +40 -5
  27. package/server/memory_worker.py +76 -11
  28. package/server/orca_handoff.py +90 -0
  29. package/server/pricing.py +161 -1
  30. package/server/prompts.py +13 -0
  31. package/server/request_uploads.py +65 -0
  32. package/server/terraform_routes.py +47 -1
  33. package/server/windows_snapshot.py +119 -0
  34. package/src/ArchitectureWorkPanel.jsx +0 -7
  35. package/src/ArtifactChecks.jsx +2 -2
  36. package/src/ChatMessage.jsx +12 -2
  37. package/src/EngagementViews.jsx +18 -16
  38. package/src/EngineeringPanel.jsx +35 -28
  39. package/src/FileTree.jsx +7 -0
  40. package/src/FinalChecklist.jsx +23 -0
  41. package/src/HelpNote.jsx +3 -0
  42. package/src/ImplementationDelivery.jsx +56 -17
  43. package/src/JourneyViews.jsx +20 -17
  44. package/src/LocationChoices.jsx +12 -0
  45. package/src/MemoryPanel.jsx +43 -31
  46. package/src/OpenAIVoicePanel.jsx +15 -14
  47. package/src/OutputsView.jsx +28 -16
  48. package/src/ProjectChecks.jsx +58 -0
  49. package/src/StarPrompt.jsx +26 -0
  50. package/src/WorkbenchDialogs.jsx +1 -1
  51. package/src/conversation.mjs +1 -0
  52. package/src/delivery.mjs +1 -1
  53. package/src/engineering.css +10 -0
  54. package/src/estimate.mjs +5 -3
  55. package/src/final-package.mjs +34 -0
  56. package/src/journey-workspace.css +39 -0
  57. package/src/journey.mjs +12 -2
  58. package/src/large-request.mjs +25 -0
  59. package/src/main.jsx +129 -98
  60. package/src/output-bundle.mjs +18 -4
  61. package/src/pricing.mjs +59 -3
  62. package/src/project-file.mjs +1 -1
  63. package/src/useChatScroll.jsx +5 -4
  64. package/src/useEngineeringStatus.mjs +17 -0
  65. package/src/validation-receipt.mjs +1 -1
  66. package/dist/assets/index-B97MkVMs.css +0 -1
@@ -2,13 +2,70 @@
2
2
  import json
3
3
  import sys
4
4
  import sqlite3
5
+ import base64
6
+ import re
5
7
  from importlib.metadata import version
8
+ from pathlib import Path
9
+ EXPECTED_ATMEM_VERSION = re.search(r"^atmem==(\d+\.\d+\.\d+)$", (Path(__file__).parent / "memory-requirements.txt").read_text(), re.M).group(1)
6
10
  from atmem import Memory
7
11
 
8
12
 
13
+ def public_record(record, owner=None, memory=None):
14
+ episode=memory.store.get_episode(record['subject_id'],record['episode_id'])if memory and record.get('episode_id')else None
15
+ raw=(episode or {}).get('raw') or record.get('raw') or {}
16
+ return {**{key:record.get(key)for key in ('id','content','status','fact_key','created_at','updated_at','supersedes_id','source_type')},
17
+ 'governance':{'userId':raw.get('user_id'), 'actor':raw.get('actor'), 'actorType':raw.get('actor_type'),
18
+ 'source':raw.get('source_url') or raw.get('interpreter'), 'ownerUserId':owner}}
19
+
20
+
21
+ def page_records(memory,subject,op):
22
+ # Pinned SDK adapter: use its policy-aware connection, never reopen the database.
23
+ from atmem.store.sqlite import _record_from_row
24
+ db=memory.store._conn
25
+ limit=max(1,min(int(op.get('limit',50)),100));query=str(op.get('query','')).strip()[:200]
26
+ status=op.get('status','active')
27
+ statuses=('active','superseded','quarantined') if status=='all' else (status,)
28
+ if any(value not in {'active','superseded','quarantined'}for value in statuses):raise ValueError('Invalid memory status')
29
+ before=None
30
+ if op.get('cursor'):
31
+ before=json.loads(base64.urlsafe_b64decode(op['cursor']).decode())
32
+ if not isinstance(before,list)or len(before)!=2 or any(not isinstance(v,str)or len(v)>100 for v in before):raise ValueError('Invalid page cursor')
33
+ candidates=[];total=0
34
+ for selected in statuses:
35
+ clauses=['subject_id=?','status=?'];args=[subject,selected]
36
+ if query:
37
+ if query.startswith('rec_'):
38
+ clauses.append('id=?');args.append(query)
39
+ else:
40
+ terms=re.findall(r'[^\W_]+',query,flags=re.UNICODE)[:12]
41
+ if not terms:raise ValueError('Enter a search word or record ID')
42
+ if not memory.store._fts_enabled:raise ValueError('Indexed search is unavailable for this store')
43
+ expression=' AND '.join('"'+word.replace('"','')+'"'for word in terms)
44
+ clauses.append('id IN (SELECT record_id FROM records_fts WHERE records_fts MATCH ? AND subject_id=?)')
45
+ args.extend([expression,subject])
46
+ where=' AND '.join(clauses)
47
+ total+=db.execute('SELECT COUNT(*) FROM records WHERE '+where,args).fetchone()[0]
48
+ if before:where+=' AND (created_at,id)<(?,?)';args.extend(before)
49
+ rows=db.execute('SELECT * FROM records WHERE '+where+' ORDER BY created_at DESC,id DESC LIMIT ?',(*args,limit+1)).fetchall()
50
+ candidates.extend(_record_from_row(row)for row in rows)
51
+ candidates.sort(key=lambda row:(row['created_at'],row['id']),reverse=True)
52
+ more=len(candidates)>limit;rows=candidates[:limit]
53
+ cursor=base64.urlsafe_b64encode(json.dumps([rows[-1]['created_at'],rows[-1]['id']]).encode()).decode()if more and rows else None
54
+ return {'records':[public_record(row,op.get('owner'),memory)for row in rows],'total':total,'hasMore':more,'nextCursor':cursor,'limit':limit}
55
+
56
+
57
+ def keyed_records(memory,subject,record_id,limit=10001):
58
+ record=memory.store.get_record(subject,record_id)
59
+ if not record or record['status']=='tombstoned':raise ValueError('Memory is unavailable in this scope')
60
+ if not record.get('fact_key'):return record,[record]
61
+ from atmem.store.sqlite import _record_from_row
62
+ rows=memory.store._conn.execute("SELECT * FROM records WHERE subject_id=? AND fact_key IS ? AND status IN ('active','superseded','quarantined') ORDER BY created_at DESC,id DESC LIMIT ?",(subject,record.get('fact_key'),limit)).fetchall()
63
+ return record,[_record_from_row(row)for row in rows]
64
+
65
+
9
66
  def run(payload):
10
- if version('atmem') != '2.3.10':
11
- raise RuntimeError('AtMem AI Architect requires the tested AtMem 2.3.10 runtime')
67
+ if version('atmem') != EXPECTED_ATMEM_VERSION:
68
+ raise RuntimeError(f'AtMem AI Architect requires the tested AtMem {EXPECTED_ATMEM_VERSION} runtime')
12
69
  memory = Memory(payload['database'], auto_vectors=False)
13
70
  receipts = sqlite3.connect(payload['database'] + '.receipts.sqlite')
14
71
  receipts.execute('CREATE TABLE IF NOT EXISTS receipts (id TEXT PRIMARY KEY, result TEXT)')
@@ -23,32 +80,40 @@ def run(payload):
23
80
  results.append(json.loads(previous[0]))
24
81
  continue
25
82
  value = memory.remember(subject, message=op['text'], source_type='user_message',
26
- interpreted_fact=op['text'], interpreted_fact_key=op['key'], actor='atforma',
27
- raw={'interpreter': 'atforma-project-event-v1', 'event_id': op['eventId'], 'source_url': op.get('source', '')})
83
+ interpreted_fact=op['text'], interpreted_fact_key=op['key'], actor=op.get('actor','atforma'),
84
+ raw={'interpreter': 'atforma-project-event-v1', 'event_id': op['eventId'], 'source_url': op.get('source', ''), 'user_id':op.get('owner'), 'actor':op.get('actor'), 'actor_type':op.get('actorType')})
28
85
  receipt = {'refused': value.get('refused', []), 'ids': [r['id'] for r in value.get('records', [])] + value.get('duplicate_ids', [])}
29
86
  receipts.execute('INSERT INTO receipts VALUES (?,?)', (op['eventId'], json.dumps(receipt)))
30
87
  receipts.commit()
31
88
  results.append(receipt)
32
89
  elif action == 'recall':
33
90
  results.append(memory.recall(subject, op['query'], limit=op.get('limit', 6)))
91
+ elif action == 'page':
92
+ results.append(page_records(memory,subject,op))
93
+ elif action == 'record':
94
+ record=memory.store.get_record(subject,op['recordId'])
95
+ if not record or record['status']=='tombstoned':raise ValueError('Record unavailable in this scope')
96
+ audit=memory.store._conn.execute('SELECT actor,event_type,created_at FROM audit_log WHERE subject_id=? AND record_id=? ORDER BY created_at DESC,sequence DESC LIMIT 50',(subject,op['recordId'])).fetchall()
97
+ results.append({'record':public_record(record,op.get('owner'),memory),'audit':[dict(row)for row in audit]})
98
+ elif action == 'forget-preview':
99
+ record,rows=keyed_records(memory,subject,op['recordId'],101)
100
+ total=memory.store._conn.execute("SELECT COUNT(*) FROM records WHERE subject_id=? AND fact_key IS ? AND status!='tombstoned'",(subject,record.get('fact_key'))).fetchone()[0]if record.get('fact_key')else 1
101
+ results.append({'records':[public_record(row,op.get('owner'),memory)for row in rows[:100]],'total':total})
34
102
  elif action == 'list':
35
103
  results.append(memory.list(subject, include_inactive=True))
36
104
  elif action == 'forget':
37
105
  # Forget the entire keyed preference history so old values cannot resurface.
38
- records = memory.list(subject, include_inactive=True)
39
- record = next((r for r in records if r['id'] == op['recordId'] and r['status'] != 'tombstoned'), None)
40
- if record is None:
41
- raise ValueError('Memory record is not available in this scope')
106
+ record,records=keyed_records(memory,subject,op['recordId'])
107
+ if len(records)>10000:raise ValueError('History exceeds the synchronous removal limit; no records were removed')
42
108
  deletion_receipts = []
43
109
  for r in records:
44
- if r.get('fact_key') == record.get('fact_key') and r['status'] != 'tombstoned':
45
- deletion_receipts.append(memory.forget_record(subject, r['id'])['receipt'])
110
+ deletion_receipts.append(memory.forget_record(subject,r['id'],actor=op.get('actor','user'))['receipt'])
46
111
  results.append({'deleted': True, 'receipts': deletion_receipts, 'key': record.get('fact_key')})
47
112
  elif action == 'audit':
48
113
  report = memory.audit(subject)
49
114
  results.append({'valid': report['audit_chain_valid'], 'events': report['audit_log'][-40:]})
50
115
  elif action == 'log':
51
- results.append(memory.log_action(subject, op['eventType'], op['payload'], actor='atforma'))
116
+ results.append(memory.log_action(subject, op['eventType'], op['payload'], actor='atmem-architect:'+op.get('owner','unknown')))
52
117
  else:
53
118
  raise ValueError('Unknown memory operation')
54
119
  return {'version': version('atmem'), 'results': results}
@@ -0,0 +1,90 @@
1
+ """Explicit local editor handoff. Never executes project files or overwrites a workspace."""
2
+ import base64
3
+ import binascii
4
+ import io
5
+ import os
6
+ from pathlib import Path, PurePosixPath
7
+ import re
8
+ import shutil
9
+ import stat
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import zipfile
14
+
15
+ MAX_ZIP = 32_000_000
16
+ MAX_EXPANDED = 150_000_000
17
+
18
+
19
+ def orca_command():
20
+ names = ['orca-ide', 'orca'] if sys.platform.startswith('linux') else ['orca']
21
+ for name in names:
22
+ found = shutil.which(name)
23
+ if found:
24
+ return found
25
+ candidate = Path.home()/'.local/bin/orca'
26
+ if candidate.is_file() and os.access(candidate, os.X_OK):
27
+ return str(candidate)
28
+ raise ValueError('Install Orca and enable its CLI in Settings → General → Orca CLI.')
29
+
30
+
31
+ def unpack_package(encoded, root, project):
32
+ try:
33
+ data = base64.b64decode(encoded, validate=True)
34
+ except (ValueError, binascii.Error):
35
+ raise ValueError('The project ZIP is invalid.') from None
36
+ if not data or len(data) > MAX_ZIP:
37
+ raise ValueError('Orca handoff supports ZIPs up to 32 MB. Download ZIP instead.')
38
+ try:
39
+ archive = zipfile.ZipFile(io.BytesIO(data))
40
+ entries = archive.infolist()
41
+ if len(entries) > 20000 or sum(e.file_size for e in entries) > MAX_EXPANDED:
42
+ raise ValueError('The expanded project exceeds the handoff limit.')
43
+ seen = set()
44
+ for entry in entries:
45
+ name = entry.filename
46
+ parts = PurePosixPath(name).parts
47
+ mode = entry.external_attr >> 16
48
+ if (not parts or name.startswith('/') or '\\' in name or '\x00' in name
49
+ or any(part in {'.', '..'} or ':' in part or part.endswith((' ', '.')) for part in parts)
50
+ or any(re.match(r'^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)', part, re.I) for part in parts)
51
+ or mode and stat.S_IFMT(mode) not in {0, stat.S_IFREG, stat.S_IFDIR}):
52
+ raise ValueError('The ZIP contains an unsupported path or file type.')
53
+ canonical = name.rstrip('/').casefold()
54
+ if canonical in seen:
55
+ raise ValueError('The ZIP contains conflicting paths.')
56
+ seen.add(canonical)
57
+ if not entries:
58
+ raise ValueError('The project ZIP is empty.')
59
+ root = Path(root)
60
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
61
+ destination = Path(tempfile.mkdtemp(prefix=project+'-', dir=root))
62
+ try:
63
+ for entry in entries:
64
+ path = destination.joinpath(*PurePosixPath(entry.filename).parts)
65
+ if entry.is_dir():
66
+ path.mkdir(parents=True, exist_ok=True)
67
+ continue
68
+ path.parent.mkdir(parents=True, exist_ok=True)
69
+ with archive.open(entry) as source, path.open('xb') as target:
70
+ shutil.copyfileobj(source, target)
71
+ os.chmod(path, 0o700 if (entry.external_attr >> 16) & 0o111 else 0o600)
72
+ except Exception:
73
+ shutil.rmtree(destination)
74
+ raise
75
+ return destination
76
+ except (zipfile.BadZipFile, RuntimeError, OSError):
77
+ raise ValueError('The project ZIP could not be prepared for Orca.') from None
78
+
79
+
80
+ def open_package(encoded, root, project):
81
+ command = orca_command()
82
+ destination = unpack_package(encoded, root, project)
83
+ try:
84
+ subprocess.run([command, 'open', '--json'], check=True, capture_output=True, timeout=30)
85
+ subprocess.run([command, 'repo', 'add', '--path', str(destination), '--kind', 'folder', '--json'],
86
+ check=True, capture_output=True, timeout=30)
87
+ except (OSError, subprocess.SubprocessError):
88
+ # Keep the prepared folder for retry/manual opening. Do not expose command output.
89
+ return {'opened': False, 'path': str(destination), 'message': 'Orca did not open. Open this folder in Orca.'}
90
+ return {'opened': True, 'path': str(destination)}
package/server/pricing.py CHANGED
@@ -61,6 +61,9 @@ def storage_quote(provider,region):
61
61
  if provider=='aws':return {**aws_s3(region),'provider':'aws','available':True}
62
62
  if provider=='azure':return dict(_azure_storage(region,int(time.time()//3600)))
63
63
  if provider=='gcp':
64
+ if region=='us-central1':
65
+ try:return dict(_google_storage_public(region,int(time.time()//3600)))
66
+ except (ValueError,OSError):pass
64
67
  import os
65
68
  key=os.getenv('GOOGLE_CLOUD_BILLING_API_KEY')
66
69
  if key and re.fullmatch(r'[a-z]+-[a-z]+\d',region):
@@ -80,5 +83,162 @@ def storage_quote(provider,region):
80
83
  price=rates[0]['unitPrice'];value=float(price.get('units',0))+float(price.get('nanos',0))/1e9
81
84
  candidates.append({'available':True,'provider':'gcp','price':value,'currency':'USD','unit':'GiB-month','region':region,'service':'Google Cloud Storage Standard / '+sku['skuId'],'source':'Google Cloud Billing Catalog','url':'https://cloud.google.com/skus/?currency=USD','date':datetime.now(timezone.utc).date().isoformat(),'coverage':'Standard storage only; operations, replication and transfer excluded'})
82
85
  if len(candidates)==1:return candidates[0]
83
- return {'available':False,'provider':'gcp','region':region,'sourceUrl':PROVIDER_PAGES[provider],'reason':'An exact regional storage SKU needs a configured Google Cloud Billing Catalog API key or a sourced quote. A generic/default-region web price will not be substituted.','nextQuestion':'Which Google Cloud region and storage class should we price?'}
86
+ return {'available':False,'provider':'gcp','region':region,'sourceUrl':PROVIDER_PAGES[provider],'reason':f'Storage pricing for {region or "the selected region"} could not be verified. Recalculate or enter a sourced rate.','nextQuestion':'Which Google Cloud region and storage class should we price?'}
84
87
  return {'available':False,'provider':'ibm','region':region,'sourceUrl':PROVIDER_PAGES[provider],'reason':'IBM pricing depends on region, storage class and plan. No verified regional quote is configured; no AWS or invented rate is substituted.','nextQuestion':'Which IBM region and storage class or existing IBM quote should we use?'}
88
+
89
+
90
+ def parse_lambda(data,region,url,architecture='x86_64'):
91
+ """Regional on-demand base meters; never apply account-specific free tiers."""
92
+ suffix='-ARM' if architecture=='arm64' else ''
93
+ expected={'requests':('AWS-Lambda-Requests'+suffix,'Request'),
94
+ 'compute':('AWS-Lambda-Duration'+suffix,'Lambda-GB-Second')}
95
+ meters={}
96
+ for name,(group,unit)in expected.items():
97
+ matches=[]
98
+ for sku,product in data.get('products',{}).items():
99
+ attrs=product.get('attributes',{})
100
+ if attrs.get('regionCode')!=region or attrs.get('group')!=group:continue
101
+ for term in data.get('terms',{}).get('OnDemand',{}).get(sku,{}).values():
102
+ for rate in term.get('priceDimensions',{}).values():
103
+ if rate.get('unit')!=unit or rate.get('beginRange')!='0':continue
104
+ value=float(rate.get('pricePerUnit',{}).get('USD','nan'))
105
+ if not 0<value<1:continue
106
+ end=rate.get('endRange')
107
+ matches.append({'price':value,'unit':unit,'service':'AWS Lambda / '+sku,
108
+ 'tierLimit':None if end=='Inf'else float(end)})
109
+ if len(matches)!=1:raise ValueError('Could not uniquely verify the regional Lambda meter')
110
+ meters[name]=matches[0]
111
+ return {'available':True,'provider':'aws','region':region,'architecture':architecture,
112
+ 'currency':'USD','source':'AWS public Price List','url':url,
113
+ 'date':datetime.now(timezone.utc).date().isoformat(),'publicationDate':data.get('publicationDate'),
114
+ 'meters':meters,'coverage':'On-demand requests and base compute only. Free tiers, logs, transfer, public gateway charges, provisioned concurrency, taxes and discounts are not included.'}
115
+
116
+
117
+ @lru_cache(maxsize=32)
118
+ def _lambda(region,architecture,hour):
119
+ url=f'https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AWSLambda/current/{region}/index.json'
120
+ with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'AtMem AI Architect/0.2'}),timeout=12)as response:raw=response.read(12_000_001)
121
+ if len(raw)>12_000_000:raise ValueError('Price list exceeds the supported size')
122
+ return parse_lambda(json.loads(raw),region,url,architecture)
123
+
124
+
125
+ def lambda_quote(region,architecture='x86_64'):
126
+ if not re.fullmatch(r'[a-z]{2}-(?:gov-)?[a-z]+-\d',region)or architecture not in {'x86_64','arm64'}:
127
+ raise ValueError('Select an explicit AWS region and Lambda architecture')
128
+ return dict(_lambda(region,architecture,int(time.time()//3600)))
129
+
130
+
131
+ def _public_rows(html):
132
+ from html.parser import HTMLParser
133
+ class Rows(HTMLParser):
134
+ def __init__(self):
135
+ super().__init__();self.rows=[];self.row=None;self.cell=None
136
+ def handle_starttag(self,tag,attrs):
137
+ if tag=='tr':self.row=[]
138
+ if tag in {'td','th'} and self.row is not None:self.cell=[]
139
+ def handle_data(self,data):
140
+ if self.cell is not None:self.cell.append(data)
141
+ def handle_endtag(self,tag):
142
+ if tag in {'td','th'} and self.cell is not None:
143
+ self.row.append(' '.join(' '.join(self.cell).split()));self.cell=None
144
+ if tag=='tr' and self.row is not None:self.rows.append(self.row);self.row=None
145
+ parser=Rows();parser.feed(html);return parser.rows
146
+
147
+
148
+ def parse_google_public(html,service,url):
149
+ import hashlib
150
+ if service=='cloud-run':
151
+ marker='Services (Requests-based billing)'
152
+ if marker not in html:raise ValueError('Cloud Run request-based pricing section is missing')
153
+ html=html[html.index(marker):]
154
+ expected={'cpu':'CPU (per vCPU-second)','memory':'Memory (per GiB-second)','requests':'Requests (per 1,000,000)'}
155
+ units={'cpu':'vCPU-seconds','memory':'GiB-seconds','requests':'million requests'}
156
+ elif service=='cloud-sql':
157
+ if 'Iowa (us-central1)' not in html or 'Enterprise Edition - N4' not in html:
158
+ raise ValueError('Cloud SQL reference region or edition is missing')
159
+ marker='Enterprise Edition - N4';html=html[html.index(marker):]
160
+ expected={'cpu':'vCPUs','memory':'Memory'};units={'cpu':'vCPU-hours','memory':'GiB-hours'}
161
+ else:raise ValueError('Unsupported Google pricing service')
162
+ meters={}
163
+ for row in _public_rows(html):
164
+ if not row:continue
165
+ for key,label in expected.items():
166
+ if key in meters or row[0]!=label:continue
167
+ value=next((re.search(r'\$([0-9]+(?:\.[0-9]+)?)',cell)for cell in row[1:]if '$' in cell),None)
168
+ if value:
169
+ price=float(value.group(1))
170
+ if not 0<price<10:raise ValueError('Unexpected Google public price')
171
+ meters[key]={'price':price,'unit':units[key]}
172
+ if len(meters)==len(expected):break
173
+ if len(meters)!=len(expected):raise ValueError('Google public pricing meters could not be verified')
174
+ return {'available':True,'provider':'gcp','service':service,'region':'us-central1','currency':'USD',
175
+ 'source':'Google Cloud public pricing','url':url,'date':datetime.now(timezone.utc).date().isoformat(),
176
+ 'sourceSha256':hashlib.sha256(html.encode()).hexdigest(),'meters':meters,
177
+ 'coverage':'US reference on-demand rates. Excludes free tiers, commitments, taxes, networking, backups, storage, external APIs and payment fees.'}
178
+
179
+
180
+ @lru_cache(maxsize=8)
181
+ def _google_public(service,hour):
182
+ path={'cloud-run':'run','cloud-sql':'sql'}.get(service)
183
+ if not path:raise ValueError('Unsupported Google pricing service')
184
+ url=f'https://cloud.google.com/{path}/pricing'
185
+ with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'AtMem Architect pricing'}),timeout=15)as response:raw=response.read(8_000_001)
186
+ if len(raw)>8_000_000:raise ValueError('Google price page exceeds its size bound')
187
+ return parse_google_public(raw.decode('utf-8'),service,url)
188
+
189
+
190
+ def google_public_quote(service,region,planning_reference=False):
191
+ if region!='us-central1' and not (planning_reference and (not region or re.search('select|unknown|tbd',region,re.I))):
192
+ raise ValueError('This public quote supports Iowa only; the selected region is preserved.')
193
+ return {**_google_public(service,int(time.time()//3600)),'planningReference':region!='us-central1'}
194
+
195
+
196
+ def parse_google_storage_public(html, region, url):
197
+ """Read an explicitly selected single-region public table, never a region list match."""
198
+ import hashlib
199
+ from html.parser import HTMLParser
200
+ if region != 'us-central1':
201
+ raise ValueError('The public default storage table verifies Iowa only.')
202
+ marker = 'id="data-storage"'
203
+ if marker not in html:
204
+ raise ValueError('Google data storage section is missing.')
205
+ section = html[html.index(marker):]
206
+ # Stop before dual-region rates. Region options alone do not verify selected pricing.
207
+ boundary = re.search(r'<(?:p|h[1-6])[^>]*>\s*(?:<b>)?Dual-region(?:</b>)?\s*</', section)
208
+ if not boundary:
209
+ raise ValueError('The single-region storage boundary is missing.')
210
+ section = section[:boundary.start()]
211
+ class Selected(HTMLParser):
212
+ def __init__(self):super().__init__();self.regions=[]
213
+ def handle_starttag(self,tag,attrs):
214
+ a=dict(attrs)
215
+ if a.get('role')=='option' and a.get('aria-selected')=='true':self.regions.append(a.get('data-value'))
216
+ selected=Selected();selected.feed(section)
217
+ if selected.regions != ['Iowa (us-central1)']:
218
+ raise ValueError('The selected public storage region is not Iowa.')
219
+ table = re.search(r'<table\b[^>]*>.*?</table>', section, re.S)
220
+ rows = _public_rows(table.group(0)) if table else []
221
+ if len(rows)!=2 or rows[0].count('Standard storage')!=1:
222
+ raise ValueError('The Standard storage table could not be verified.')
223
+ index=rows[0].index('Standard storage')
224
+ if index>=len(rows[1]):raise ValueError('The storage price is missing.')
225
+ cell=rows[1][index]
226
+ rate=re.fullmatch(r'\$([0-9]+(?:\.[0-9]+)?) / 1 gibibyte hour(?:, per 1 month / account)?',cell)
227
+ if not rate or not 0<float(rate.group(1))<1:raise ValueError('Unexpected Google storage rate or unit.')
228
+ hourly=float(rate.group(1))
229
+ return {'available':True,'provider':'gcp','price':hourly*730,'currency':'USD',
230
+ 'unit':'GiB-month','region':region,'service':'Cloud Storage Standard / single-region',
231
+ 'source':'Google Cloud public pricing','url':url,
232
+ 'date':datetime.now(timezone.utc).date().isoformat(),
233
+ 'sourceSha256':hashlib.sha256(section.encode()).hexdigest(),
234
+ 'hourlyPrice':hourly,'hoursPerMonth':730,
235
+ 'coverage':'Single-region Standard storage; 730-hour planning month. Operations, replication, transfer, taxes and discounts excluded.'}
236
+
237
+
238
+ @lru_cache(maxsize=32)
239
+ def _google_storage_public(region,hour):
240
+ url='https://cloud.google.com/storage/pricing'
241
+ with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'AtMem Architect pricing'}),timeout=12) as response:
242
+ raw=response.read(8_000_001)
243
+ if len(raw)>8_000_000:raise ValueError('Google storage page exceeds its size bound.')
244
+ return parse_google_storage_public(raw.decode('utf-8'),region,url)
package/server/prompts.py CHANGED
@@ -121,3 +121,16 @@ new project; preserve an existing client's chosen name. The host constructs a co
121
121
  proposal after validation. Do not spend output tokens copying the entire current graph,
122
122
  original brief or unchanged configuration just to propose a small correction.
123
123
  """
124
+
125
+
126
+ SYSTEM_PROMPT += """
127
+ # Owner UX instruction: avoid word vomit
128
+ Keep the visible chat reply quick and concise: normally one or two short sentences,
129
+ at most 20 words total. Longer optional explanation belongs behind a help disclosure or in the specification. Ask one direct question when a decision is needed.
130
+ Do not recap the brief, repeat accepted choices, narrate your process, advertise a
131
+ skill or add examples unless asked. Example: 'Use USD for welcome credit and top-ups?'
132
+ Preserve full requirements, rationale, uncertainty and technical detail in structured
133
+ project/specification fields. Do not shorten or discard source requirements.
134
+ If a material blocker needs explaining, state its cause and next action briefly.
135
+ User requests for detailed specifications still receive complete specifications.
136
+ """
@@ -0,0 +1,65 @@
1
+ """Bounded, temporary, ordered request assembly. Uploaded content is never executed."""
2
+ import hashlib
3
+ import secrets
4
+ import tempfile
5
+ import threading
6
+ import time
7
+
8
+ MAX_BYTES = 64 * 1024 * 1024
9
+ CHUNK_BYTES = 128 * 1024
10
+
11
+
12
+ class RequestUploads:
13
+ def __init__(self):
14
+ self.rows = {}
15
+ self.lock = threading.Lock()
16
+
17
+ def _expire(self):
18
+ for key, row in list(self.rows.items()):
19
+ if time.monotonic() - row['updated'] > 1800:
20
+ self.rows.pop(key)['file'].close()
21
+
22
+ def start(self, size, digest):
23
+ with self.lock:
24
+ self._expire()
25
+ if not 0 < size <= MAX_BYTES:
26
+ raise ValueError('Request exceeds the 64 MiB upload limit.')
27
+ if len(self.rows) >= 8 or sum(r['size'] for r in self.rows.values()) + size > 2 * MAX_BYTES:
28
+ raise ValueError('Upload capacity is busy. Retry after current uploads finish.')
29
+ key = secrets.token_hex(24)
30
+ self.rows[key] = dict(file=tempfile.TemporaryFile(), size=size, digest=digest,
31
+ received=0, index=0, updated=time.monotonic())
32
+ return {'uploadId': key, 'chunkBytes': CHUNK_BYTES}
33
+
34
+ def append(self, key, index, content):
35
+ with self.lock:
36
+ self._expire()
37
+ row = self.rows.get(key)
38
+ if row is None:
39
+ raise ValueError('Upload expired. Start validation again.')
40
+ if index != row['index'] or not 0 < len(content) <= CHUNK_BYTES or row['received'] + len(content) > row['size']:
41
+ raise ValueError('Upload chunk is out of order or exceeds its declared size.')
42
+ row['file'].write(content)
43
+ row.update(received=row['received'] + len(content), index=index + 1, updated=time.monotonic())
44
+ return {'receivedBytes': row['received']}
45
+
46
+ def take(self, key):
47
+ with self.lock:
48
+ self._expire()
49
+ row = self.rows.pop(key, None)
50
+ if row is None:
51
+ raise ValueError('Upload expired. Start validation again.')
52
+ try:
53
+ row['file'].seek(0)
54
+ data = row['file'].read()
55
+ if len(data) != row['size'] or hashlib.sha256(data).hexdigest() != row['digest']:
56
+ raise ValueError('Upload is incomplete or its checksum changed. Retry validation.')
57
+ return data
58
+ finally:
59
+ row['file'].close()
60
+
61
+ def cancel(self, key):
62
+ with self.lock:
63
+ row = self.rows.pop(key, None)
64
+ if row:
65
+ row['file'].close()
@@ -11,13 +11,49 @@ from pathlib import Path
11
11
  import shutil
12
12
  import subprocess
13
13
  import tempfile
14
+ import base64
15
+ import binascii
14
16
  from fastapi import APIRouter, HTTPException
15
- from pydantic import BaseModel, ConfigDict
17
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
18
+ from .request_uploads import RequestUploads, MAX_BYTES
16
19
  from typing import Literal
17
20
 
18
21
  router = APIRouter()
19
22
  LOCK = asyncio.Lock()
20
23
  ROOT = Path(__file__).resolve().parent.parent
24
+ UPLOADS = RequestUploads()
25
+
26
+ class UploadStart(BaseModel):
27
+ model_config = ConfigDict(extra='forbid')
28
+ bytes: int = Field(gt=0, le=MAX_BYTES, strict=True)
29
+ sha256: str = Field(pattern=r'^[a-f0-9]{64}$')
30
+
31
+ class UploadId(BaseModel):
32
+ model_config = ConfigDict(extra='forbid')
33
+ uploadId: str = Field(pattern=r'^[a-f0-9]{48}$')
34
+
35
+ class UploadChunk(UploadId):
36
+ index: int = Field(ge=0, strict=True)
37
+ data: str = Field(min_length=1, max_length=174764)
38
+
39
+ @router.post('/api/terraform/uploads/start')
40
+ async def upload_start(payload: UploadStart):
41
+ try:
42
+ return UPLOADS.start(payload.bytes, payload.sha256)
43
+ except ValueError as error:
44
+ raise HTTPException(409, str(error))
45
+
46
+ @router.post('/api/terraform/uploads/chunk')
47
+ async def upload_chunk(payload: UploadChunk):
48
+ try:
49
+ return UPLOADS.append(payload.uploadId, payload.index, base64.b64decode(payload.data, validate=True))
50
+ except (ValueError, binascii.Error) as error:
51
+ raise HTTPException(422, str(error))
52
+
53
+ @router.post('/api/terraform/uploads/cancel')
54
+ async def upload_cancel(payload: UploadId):
55
+ UPLOADS.cancel(payload.uploadId)
56
+ return {'cancelled': True}
21
57
 
22
58
  class ValidationRequest(BaseModel):
23
59
  model_config = ConfigDict(extra='forbid')
@@ -116,3 +152,13 @@ async def validate_route(payload: ValidationRequest):
116
152
  raise HTTPException(504, 'Terraform validation timed out. No deployment was attempted.')
117
153
  except (ValueError, OSError):
118
154
  raise HTTPException(422, 'Could not validate the generated Terraform draft. Check the project and local Terraform installation.')
155
+
156
+ @router.post('/api/terraform/validate-upload')
157
+ async def validate_upload(payload: UploadId):
158
+ if LOCK.locked():
159
+ raise HTTPException(409, 'A Terraform validation is already running.')
160
+ try:
161
+ request = ValidationRequest.model_validate_json(UPLOADS.take(payload.uploadId))
162
+ except (ValueError, ValidationError):
163
+ raise HTTPException(422, 'The upload is incomplete, changed, or is not a valid project validation request.')
164
+ return await validate_route(request)