every-app-is-open-source 0.1.0__py3-none-any.whl

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.
eaos/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Small, evidence-linked native-analysis tools for AI agents."""
2
+
3
+ __version__ = "0.1.0"
eaos/backend.py ADDED
@@ -0,0 +1,246 @@
1
+ """Small PyGhidra adapter. A digest-keyed persistent project lives beside the binary."""
2
+ from __future__ import annotations
3
+
4
+ import fcntl
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+
14
+ from .evidence import Evidence
15
+ from .swift import decode_small_string
16
+ from .runtime import configure
17
+
18
+
19
+ class Backend:
20
+ def __init__(self, target: str):
21
+ self.target = Path(target).expanduser().resolve(strict=True)
22
+ if not self.target.is_file():
23
+ raise ValueError("Target must be a regular file")
24
+ self.digest = hashlib.sha256(self.target.read_bytes()).hexdigest()
25
+ self.directory = self.target.parent / 'eaos-analysis' / self.digest
26
+ self.directory.mkdir(parents=True, exist_ok=True)
27
+ self.evidence = Evidence(self.directory / 'evidence', self.digest)
28
+ self.program = self.project = self.consumer = self.decompiler = None
29
+ self.lock = None
30
+
31
+ def _load(self):
32
+ if self.program is not None:
33
+ return
34
+ self.lock = (self.directory / 'session.lock').open('a+')
35
+ try:
36
+ fcntl.flock(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
37
+ except BlockingIOError:
38
+ self.lock.close()
39
+ self.lock = None
40
+ raise RuntimeError("Program is already open in another EAOS process; close it before retrying")
41
+ tools, ghidra, jdk = configure()
42
+ from pyghidra.launcher import HeadlessPyGhidraLauncher
43
+ import pyghidra
44
+ if not pyghidra.started():
45
+ launcher = HeadlessPyGhidraLauncher(install_dir=ghidra)
46
+ state = tools / 'java-state'; state.mkdir(parents=True, exist_ok=True)
47
+ launcher.add_vmargs('-Xmx4G', f'-Duser.home={state}')
48
+ launcher.start()
49
+ from ghidra.util.task import TaskMonitor
50
+ from ghidra.program.util import GhidraProgramUtilities
51
+ from ghidra.app.decompiler import DecompInterface
52
+ monitor = TaskMonitor.DUMMY
53
+ project_dir = self.directory / 'ghidra'; project_dir.mkdir(exist_ok=True)
54
+ self.project = pyghidra.open_project(project_dir, 'program', create=True)
55
+ started = time.monotonic()
56
+ imported = self.project.getProjectData().getFile('/'+self.target.name) is None
57
+ if imported:
58
+ results = pyghidra.program_loader().project(self.project).source(str(self.target)).name(self.target.name).load()
59
+ try:
60
+ results.getPrimary().save(monitor)
61
+ finally:
62
+ results.close()
63
+ self.program, self.consumer = pyghidra.consume_program(self.project, '/'+self.target.name)
64
+ analyzed = False
65
+ if imported or not GhidraProgramUtilities.isAnalyzed(self.program):
66
+ log = pyghidra.analyze(self.program)
67
+ (self.directory / 'analysis.log').write_text(str(log))
68
+ self.program.save('EAOS initial analysis', monitor)
69
+ analyzed = True
70
+ metadata_path = self.directory / 'analysis.json'
71
+ metadata = json.loads(metadata_path.read_text()) if metadata_path.exists() else {}
72
+ metadata.update(target=str(self.target), sha256=self.digest,
73
+ ghidra_version='12.1.4', language=str(self.program.getLanguageID()))
74
+ if analyzed:
75
+ metadata['analysis_count'] = metadata.get('analysis_count', 0) + 1
76
+ metadata['analysis_seconds'] = time.monotonic() - started
77
+ metadata_path.write_text(json.dumps(metadata, indent=2))
78
+ self.decompiler = DecompInterface()
79
+ self.decompiler.openProgram(self.program)
80
+
81
+ def close(self):
82
+ if self.decompiler is not None:
83
+ self.decompiler.dispose()
84
+ self.decompiler = None
85
+ if self.program is not None:
86
+ self.program.release(self.consumer)
87
+ self.program = None
88
+ if self.project is not None:
89
+ self.project.close()
90
+ self.project = None
91
+ if self.lock is not None:
92
+ fcntl.flock(self.lock, fcntl.LOCK_UN); self.lock.close(); self.lock = None
93
+
94
+ def result(self, operation, classification, data, limitations=None, preview=None):
95
+ evidence = self.evidence.save(operation, classification, data, limitations)
96
+ return {'classification': classification, 'data': data if preview is None else preview,
97
+ 'limitations': limitations or [],
98
+ 'evidence': {'id': evidence['id'], 'uri': evidence['uri']}}
99
+
100
+ def address(self, address):
101
+ result = self.program.getAddressFactory().getAddress(str(address))
102
+ if result is None:
103
+ raise ValueError('Invalid program address')
104
+ return result
105
+
106
+ def function(self, address):
107
+ addr = self.address(address)
108
+ fn = self.program.getFunctionManager().getFunctionContaining(addr)
109
+ if fn is None:
110
+ raise ValueError('No analyzed function contains this address; inspect references or memory')
111
+ return fn
112
+
113
+ def fn_info(self, fn):
114
+ return {'address': str(fn.getEntryPoint()), 'name': str(fn.getName()),
115
+ 'signature': str(fn.getSignature()), 'size': int(fn.getBody().getNumAddresses())}
116
+
117
+ def open_program(self, full=False):
118
+ self._load()
119
+ blocks = [{'name':str(b.getName()),'start':str(b.getStart()),'end':str(b.getEnd()),
120
+ 'read':bool(b.isRead()),'write':bool(b.isWrite()),'execute':bool(b.isExecute())}
121
+ for b in self.program.getMemory().getBlocks()]
122
+ data = {'target': str(self.target), 'sha256': self.digest,
123
+ 'language': str(self.program.getLanguageID()),
124
+ 'function_count': int(self.program.getFunctionManager().getFunctionCount()),
125
+ 'memory': blocks, 'cache':json.loads((self.directory/'analysis.json').read_text())}
126
+ preview = data if full else {k:data[k] for k in ('sha256','language','function_count')}
127
+ if not full:
128
+ preview['analysis_count'] = data['cache'].get('analysis_count')
129
+ preview['memory'] = [{'name':b['name'],'start':b['start'],'end':b['end'],
130
+ 'permissions': ''.join(c if b[k] else '-' for c,k in [('r','read'),('w','write'),('x','execute')])} for b in blocks]
131
+ return self.result('open_program','computed',data,
132
+ ['Recovered functions/types are computed, not original source.'], preview)
133
+
134
+ def search(self, kind='strings', pattern='', offset=0, limit=30, full=False):
135
+ self._load()
136
+ if not 0 <= offset or not 1 <= limit <= 200:
137
+ raise ValueError('offset must be nonnegative and limit 1..200')
138
+ pattern = pattern.casefold()
139
+ rows = []
140
+ if kind == 'functions':
141
+ for fn in self.program.getFunctionManager().getFunctions(True):
142
+ item = self.fn_info(fn)
143
+ if pattern in (item['name']+' '+item['signature']).casefold(): rows.append(item)
144
+ elif kind == 'strings':
145
+ from java.lang import String
146
+ for item in self.program.getListing().getDefinedData(True):
147
+ value = item.getValue()
148
+ if not isinstance(value, (str, String)): continue
149
+ text = str(value)
150
+ if pattern not in text.casefold(): continue
151
+ refs = list(self.program.getReferenceManager().getReferencesTo(item.getAddress()))
152
+ rows.append({'address':str(item.getAddress()),'text':text,
153
+ 'references':[{'address':str(r.getFromAddress()),
154
+ 'function':self.fn_info(fn) if (fn:=self.program.getFunctionManager().getFunctionContaining(r.getFromAddress())) else None}
155
+ for r in refs]})
156
+ else:
157
+ raise ValueError('kind must be strings or functions')
158
+ page = rows[offset:offset+limit]
159
+ data = {'kind':kind,'matches':len(rows),'items':page,
160
+ 'next_offset':offset+limit if offset+limit<len(rows) else None}
161
+ preview = json.loads(json.dumps(data))
162
+ if not full:
163
+ for item in preview['items']:
164
+ item.pop('signature',None); item.pop('size',None)
165
+ for ref in item.get('references',[]):
166
+ if ref['function']:
167
+ ref['function'] = {'address':ref['function']['address']}
168
+ return self.result('search','computed',data,
169
+ ['Only Ghidra-defined strings/functions; other text may be absent.'], preview)
170
+
171
+ def inspect(self, addresses, mode='decompile', full=False):
172
+ self._load()
173
+ if not 1 <= len(addresses) <= 8: raise ValueError('Supply 1..8 addresses')
174
+ if mode not in {'decompile','assembly','references','calls','all'}:
175
+ raise ValueError('mode must be decompile, assembly, references, calls or all')
176
+ from ghidra.util.task import TaskMonitor
177
+ rows = []
178
+ for address in addresses:
179
+ addr = self.address(address)
180
+ if mode == 'references':
181
+ refs = [{'from':str(r.getFromAddress()),'to':str(r.getToAddress()),
182
+ 'type':str(r.getReferenceType()),
183
+ 'function':self.fn_info(fn) if (fn:=self.program.getFunctionManager().getFunctionContaining(r.getFromAddress())) else None}
184
+ for r in self.program.getReferenceManager().getReferencesTo(addr)]
185
+ rows.append({'address':str(addr),'references':refs});continue
186
+ fn = self.function(address); row = self.fn_info(fn)
187
+ if mode in {'decompile','all'}:
188
+ result = self.decompiler.decompileFunction(fn, 60, TaskMonitor.DUMMY)
189
+ if not result.decompileCompleted(): raise RuntimeError(str(result.getErrorMessage()))
190
+ code = str(result.getDecompiledFunction().getC())
191
+ row['pseudocode'] = code
192
+ path = self.directory/'evidence'/f'function-{fn.getEntryPoint()}.c'
193
+ path.write_text(code)
194
+ if mode in {'assembly','all'}:
195
+ row['instructions'] = [{'address':str(i.getAddress()), 'text':str(i),
196
+ 'bytes':bytes((int(b)&255) for b in i.getBytes()).hex()}
197
+ for i in self.program.getListing().getInstructions(fn.getBody(),True)]
198
+ if mode in {'calls','all'} or (mode=='decompile' and full):
199
+ row['callers'] = sorted([self.fn_info(f) for f in fn.getCallingFunctions(TaskMonitor.DUMMY)],key=lambda f:f['address'])
200
+ row['callees'] = sorted([self.fn_info(f) for f in fn.getCalledFunctions(TaskMonitor.DUMMY)],key=lambda f:f['address'])
201
+ rows.append(row)
202
+ preview = json.loads(json.dumps(rows))
203
+ for row in preview:
204
+ if 'pseudocode' in row:
205
+ cap = 32000 if full else 5000
206
+ row['truncated'] = len(row['pseudocode']) > cap
207
+ row['pseudocode'] = row['pseudocode'][:cap]
208
+ for field in ['instructions','references','callers','callees']:
209
+ if field in row:
210
+ count = 500 if full else (64 if field=='instructions' else 30)
211
+ row[field+'_total'] = len(row[field]);row[field] = row[field][:count]
212
+ return self.result('inspect','fact' if mode=='assembly' else 'computed',rows,
213
+ ['Pseudocode, types and call graph are computed static interpretations; indirect calls may be unresolved.'] if mode!='assembly' else [], preview)
214
+
215
+ def read_memory(self, address, length=64, format='hex'):
216
+ self._load()
217
+ if format not in {'hex','utf8','both'}: raise ValueError('format must be hex, utf8 or both')
218
+ if not 1 <= length <= 65536: raise ValueError('length must be 1..65536')
219
+ import jpype
220
+ raw = jpype.JArray(jpype.JByte)(length)
221
+ count = self.program.getMemory().getBytes(self.address(address),raw)
222
+ data = bytes(int(x)&255 for x in raw[:count])
223
+ record = {'address':address,'length':count,'hex':data.hex(),
224
+ 'utf8':data.decode('utf-8',errors='replace')}
225
+ preview = {k:v for k,v in record.items() if k not in {'hex','utf8'} or format=='both' or k==format}
226
+ return self.result('read_memory','fact',record,preview=preview)
227
+
228
+ def calculate(self, expression):
229
+ process = subprocess.run([sys.executable,'-I',str(Path(__file__).with_name('calculator.py'))],
230
+ input=json.dumps({'expression':expression}),capture_output=True,text=True,timeout=4)
231
+ if process.returncode: raise ValueError(process.stderr.strip())
232
+ return self.result('calculate','computed',{'expression':expression,'value':process.stdout.strip()},
233
+ ['Arithmetic establishes the supplied expression, not whether its premises match the binary.'])
234
+
235
+ def decode_swift_string(self, words=None, address=None):
236
+ limitations = ['Supports Swift small String storage only; no pointer-backed or Objective-C tagged strings.']
237
+ if address is not None:
238
+ raw = bytes.fromhex(self.read_memory(address,16)['data']['hex'])
239
+ words = [int.from_bytes(raw[:8],'little'),int.from_bytes(raw[8:],'little')]
240
+ else:
241
+ limitations.append('Words were supplied by the caller; their presence in this binary is not established.')
242
+ if words is None: raise ValueError('Supply words or a program address')
243
+ return self.result('decode_swift_string','computed',decode_small_string(words),limitations)
244
+
245
+ def read_evidence(self, id, offset=0, limit=12000):
246
+ return self.evidence.read(id,offset,limit)
eaos/calculator.py ADDED
@@ -0,0 +1,50 @@
1
+ """Pure bounded calculator. Executed in a time-limited child process by the tool."""
2
+ import ast
3
+ import json
4
+ import sys
5
+
6
+ NAMES = {k: v for k, v in dict(int=int, str=str, bytes=bytes, len=len, hex=hex,
7
+ bin=bin, abs=abs, min=min, max=max, round=round, sum=sum, list=list, tuple=tuple,
8
+ chr=chr, ord=ord, range=range, enumerate=enumerate, zip=zip, sorted=sorted,
9
+ float=float).items()}
10
+ ATTRS = {'to_bytes','from_bytes','fromhex','hex','decode','encode','replace','split','join','strip'}
11
+ ALLOWED = (ast.Expression,ast.Constant,ast.List,ast.Tuple,ast.Dict,ast.Set,ast.Load,
12
+ ast.Store,ast.BinOp,ast.UnaryOp,ast.BoolOp,ast.Compare,ast.IfExp,ast.Subscript,
13
+ ast.Slice,ast.Call,ast.Name,ast.Attribute,ast.keyword,ast.Add,ast.Sub,ast.Mult,
14
+ ast.Div,ast.FloorDiv,ast.Mod,ast.Pow,ast.BitAnd,ast.BitOr,ast.BitXor,ast.LShift,
15
+ ast.RShift,ast.UAdd,ast.USub,ast.Invert,ast.And,ast.Or,ast.Not,ast.Eq,ast.NotEq,
16
+ ast.Lt,ast.LtE,ast.Gt,ast.GtE,ast.In,ast.NotIn,ast.ListComp,ast.SetComp,
17
+ ast.DictComp,ast.GeneratorExp,ast.comprehension,ast.Lambda,ast.arguments,ast.arg)
18
+
19
+
20
+ def calculate(expression):
21
+ if len(expression) > 10000:
22
+ raise ValueError("Expression too long")
23
+ tree = ast.parse(expression, mode='eval')
24
+ nodes = list(ast.walk(tree))
25
+ if len(nodes) > 2000:
26
+ raise ValueError("Expression too complex")
27
+ bound = {n.arg for n in nodes if isinstance(n, ast.arg)} | {
28
+ n.id for n in nodes if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Store)}
29
+ for node in nodes:
30
+ if not isinstance(node, ALLOWED):
31
+ raise ValueError("Unsupported expression")
32
+ if isinstance(node, ast.Name) and node.id not in NAMES and node.id not in bound:
33
+ raise ValueError("Unsupported name")
34
+ if isinstance(node, ast.Attribute) and node.attr not in ATTRS:
35
+ raise ValueError("Unsupported attribute")
36
+ return repr(eval(compile(tree, '<calculator>', 'eval'), {'__builtins__': {}, **NAMES}, {}))
37
+
38
+
39
+ if __name__ == '__main__':
40
+ try:
41
+ import resource
42
+ resource.setrlimit(resource.RLIMIT_CPU, (2, 2))
43
+ # Darwin advertises RLIMIT_AS but rejects setting it (EINVAL). Keep the
44
+ # CPU limit and caller's four-second timeout there; do not fail every sum.
45
+ if sys.platform != 'darwin':
46
+ resource.setrlimit(resource.RLIMIT_AS, (512*1024*1024, 512*1024*1024))
47
+ print(calculate(json.load(sys.stdin)['expression']))
48
+ except Exception as exc:
49
+ print(str(exc), file=sys.stderr)
50
+ sys.exit(1)
eaos/cli.py ADDED
@@ -0,0 +1,97 @@
1
+ """CLI and stdio MCP, using only the standard library for transport."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import subprocess
9
+ import sys
10
+
11
+ from .backend import Backend
12
+
13
+
14
+ def schema(properties=None, required=None):
15
+ return {'type':'object','properties':properties or {},'required':required or [],'additionalProperties':False}
16
+
17
+
18
+ S = {'type':'string'}
19
+ B = {'type':'boolean','default':False}
20
+ TOOLS = [
21
+ ('open_program','Selected target, memory map and analysis cache. full=true includes all metadata.',schema({'full':B})),
22
+ ('search','Substring search of strings (with xrefs) or functions. Page with next_offset; full=true for signatures.',schema({'kind':{'type':'string','enum':['strings','functions']},'pattern':S,'offset':{'type':'integer','minimum':0},'limit':{'type':'integer','minimum':1,'maximum':200},'full':B})),
23
+ ('inspect','Batch addresses. Default computed pseudocode. mode=calls, references, assembly or all; full=true expands. Omitted detail: read_evidence.',schema({'addresses':{'type':'array','items':S,'minItems':1,'maxItems':8},'mode':{'type':'string','enum':['decompile','assembly','references','calls','all']},'full':B},['addresses'])),
24
+ ('read_memory','Fact: literal bytes at a virtual address. Default hex; format=utf8 or both for text display.',schema({'address':S,'length':{'type':'integer','minimum':1,'maximum':65536},'format':{'type':'string','enum':['hex','utf8','both']}},['address'])),
25
+ ('calculate','Pure Python arithmetic/bytes expression. No imports, filesystem, network or program execution. Useful for numeric tables and packed constants.',schema({'expression':S},['expression'])),
26
+ ('decode_swift_string','Decode 64-bit little-endian Swift small String storage. Supply address or two unsigned words. Reject unsupported pointer layouts instead of inventing text.',schema({'words':{'type':'array','items':{'anyOf':[S,{'type':'integer'}]},'minItems':2,'maxItems':2},'address':S})),
27
+ ('read_evidence','Retrieve a content-checked evidence record by its returned id, with character paging. Never reads arbitrary paths.',schema({'id':S,'offset':{'type':'integer','minimum':0},'limit':{'type':'integer','minimum':1,'maximum':40000}},['id'])),
28
+ ]
29
+
30
+
31
+ def protect_stdout():
32
+ sys.stdout.flush()
33
+ duplicate = os.dup(1)
34
+ os.dup2(2, 1)
35
+ sys.stdout = os.fdopen(duplicate, 'w', encoding='utf-8', buffering=1)
36
+
37
+
38
+ def serve(backend):
39
+ catalog = [{'name':name,'description':description,'inputSchema':spec} for name,description,spec in TOOLS]
40
+ allowed = {t['name'] for t in catalog}
41
+ for line in sys.stdin:
42
+ try:
43
+ request = json.loads(line)
44
+ except ValueError:
45
+ continue
46
+ if 'id' not in request:
47
+ continue
48
+ try:
49
+ method = request['method']
50
+ if method == 'initialize':
51
+ result = {'protocolVersion':request.get('params',{}).get('protocolVersion','2024-11-05'),
52
+ 'capabilities':{'tools':{}},'serverInfo':{'name':'every-app-is-open-source','version':'0.1.0'}}
53
+ elif method == 'ping': result = {}
54
+ elif method == 'tools/list': result = {'tools':catalog}
55
+ elif method == 'tools/call':
56
+ name = request['params']['name']
57
+ if name not in allowed: raise ValueError('Unknown tool')
58
+ try:
59
+ output = getattr(backend,name)(**request['params'].get('arguments',{}))
60
+ result = {'content':[{'type':'text','text':json.dumps(output,ensure_ascii=False,separators=(',',':'))}]}
61
+ except Exception as exc:
62
+ result = {'isError':True,'content':[{'type':'text','text':json.dumps({'error':str(exc),'classification':'fact','scope':'tool failure; no analysis claim'})}]}
63
+ else: raise ValueError('Unsupported method')
64
+ response = {'jsonrpc':'2.0','id':request['id'],'result':result}
65
+ except Exception as exc:
66
+ response = {'jsonrpc':'2.0','id':request['id'],'error':{'code':-32603,'message':str(exc)}}
67
+ print(json.dumps(response,ensure_ascii=False),flush=True)
68
+
69
+
70
+ def main():
71
+ parser = argparse.ArgumentParser(description='Every App Is Open Source: small native analysis tools')
72
+ sub = parser.add_subparsers(dest='command',required=True)
73
+ setup = sub.add_parser('setup',help='Download verified Java/Ghidra and build the Apple Silicon decompiler')
74
+ setup.add_argument('--rebuild',action='store_true',help='Rebuild the ARM64 decompiler')
75
+ setup.add_argument('--archive-cache',type=Path,
76
+ help='Use an existing directory of checksum-verified runtime archives')
77
+ server = sub.add_parser('mcp'); server.add_argument('binary')
78
+ call = sub.add_parser('call');call.add_argument('binary');call.add_argument('tool',choices=[t[0] for t in TOOLS]);call.add_argument('--args',default='{}')
79
+ args = parser.parse_args()
80
+ if args.command == 'setup':
81
+ from .runtime import install
82
+ try:
83
+ install(args.rebuild, args.archive_cache.expanduser().resolve() if args.archive_cache else None)
84
+ except (RuntimeError,OSError,subprocess.CalledProcessError) as exc:
85
+ parser.exit(1, f'EAOS setup failed: {exc}\n')
86
+ return
87
+ protect_stdout()
88
+ backend = Backend(args.binary)
89
+ try:
90
+ if args.command == 'mcp': serve(backend)
91
+ else: print(json.dumps(getattr(backend,args.tool)(**json.loads(args.args)),ensure_ascii=False,indent=2))
92
+ finally:
93
+ backend.close()
94
+
95
+
96
+ if __name__ == '__main__':
97
+ main()
@@ -0,0 +1,10 @@
1
+ {
2
+ "ghidra.zip": {
3
+ "sha256": "ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db",
4
+ "bytes": 569649598
5
+ },
6
+ "jdk.tar.gz": {
7
+ "sha256": "3623232f33a9c3baadf304480b2535f9a3cba8a58d42ecbb438ba267315d9998",
8
+ "bytes": 200073404
9
+ }
10
+ }
eaos/evidence.py ADDED
@@ -0,0 +1,47 @@
1
+ """Durable, content-checked evidence stored alongside the input program."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+
10
+ class Evidence:
11
+ def __init__(self, directory: Path, target_sha256: str):
12
+ self.directory = directory
13
+ directory.mkdir(parents=True, exist_ok=True)
14
+ self.target_sha256 = target_sha256
15
+
16
+ def save(self, operation: str, classification: str, data, limitations=None):
17
+ if classification not in {"fact", "computed", "guess"}:
18
+ raise ValueError("Unknown evidence classification")
19
+ payload = {"target_sha256": self.target_sha256,
20
+ "operation": operation, "classification": classification,
21
+ "limitations": limitations or [], "data": data}
22
+ canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode()
23
+ identifier = hashlib.sha256(canonical).hexdigest()[:32]
24
+ path = self.directory / f"{identifier}.json"
25
+ record = {"id": identifier, **payload}
26
+ raw = json.dumps(record, ensure_ascii=False, indent=2, sort_keys=True).encode()
27
+ if path.exists() and path.read_bytes() != raw:
28
+ raise ValueError('Evidence identity collision or existing record changed')
29
+ path.write_bytes(raw)
30
+ digest = hashlib.sha256(raw).hexdigest()
31
+ (self.directory / f"{identifier}.sha256").write_text(digest + "\n")
32
+ return {"id": identifier, "path": str(path), "uri": path.as_uri(),
33
+ "sha256": digest, "target_sha256": self.target_sha256}
34
+
35
+ def read(self, identifier: str, offset: int = 0, limit: int = 12000):
36
+ if not re.fullmatch(r"[0-9a-f]{32}", identifier):
37
+ raise ValueError("Use an evidence id returned by this program's tools")
38
+ if offset < 0 or not 1 <= limit <= 40000:
39
+ raise ValueError("offset must be nonnegative; limit must be 1..40000")
40
+ path = self.directory / f"{identifier}.json"
41
+ raw = path.read_bytes()
42
+ if hashlib.sha256(raw).hexdigest() != (path.with_suffix('.sha256')).read_text().strip():
43
+ raise ValueError("Evidence hash mismatch")
44
+ text = raw.decode()
45
+ return {"classification": "fact", "id": identifier, "uri": path.as_uri(),
46
+ "offset": offset, "text": text[offset:offset+limit],
47
+ "next_offset": offset + limit if offset + limit < len(text) else None}
eaos/runtime.py ADDED
@@ -0,0 +1,189 @@
1
+ """Verified Java/Ghidra installation shared by wheels and checkout setup."""
2
+ from pathlib import Path
3
+ from importlib.resources import files
4
+ from importlib.metadata import version, PackageNotFoundError
5
+ import fcntl
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import platform
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tarfile
14
+ import tempfile
15
+ import urllib.request
16
+
17
+ GHIDRA = 'ghidra_12.1.4_PUBLIC'
18
+ JDK = 'jdk-21.0.12.1+1'
19
+ BUILD_ARGS = ['make','-B','-o','xml.cc','-o','grammar.cc','-o','pcodeparse.cc',
20
+ '-o','slghparse.cc','-o','slghscan.cc','-j4','ghidra_opt',
21
+ 'CXX=clang++ -std=c++11','ARCH_TYPE=-arch arm64',
22
+ 'ADDITIONAL_FLAGS=-mmacosx-version-min=11.0 -w']
23
+
24
+
25
+ def digest(path):
26
+ with path.open('rb') as source:
27
+ return hashlib.file_digest(source, 'sha256').hexdigest()
28
+
29
+
30
+ def verify_archive(path, pin):
31
+ if path.stat().st_size != pin['bytes'] or digest(path) != pin['sha256']:
32
+ raise RuntimeError(f'Archive integrity check failed: {path}. Keep it for inspection; supply a valid archive before retrying.')
33
+
34
+
35
+ def download(url, destination, pin):
36
+ """An interrupted or corrupt download never becomes the cached archive."""
37
+ destination.parent.mkdir(parents=True, exist_ok=True)
38
+ temporary = None
39
+ try:
40
+ with tempfile.NamedTemporaryFile(dir=destination.parent, suffix='.part', delete=False) as out:
41
+ temporary = Path(out.name)
42
+ request = urllib.request.Request(url, headers={'User-Agent':'EAOS-setup'})
43
+ with urllib.request.urlopen(request, timeout=60) as response:
44
+ shutil.copyfileobj(response, out, length=1024*1024)
45
+ verify_archive(temporary, pin)
46
+ temporary.replace(destination)
47
+ finally:
48
+ if temporary is not None:
49
+ temporary.unlink(missing_ok=True)
50
+
51
+
52
+ def archive_url(name):
53
+ if name == 'jdk.tar.gz':
54
+ return 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12.1%2B1/OpenJDK21U-jdk_aarch64_mac_hotspot_21.0.12.1_1.tar.gz'
55
+ request = urllib.request.Request(
56
+ 'https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_12.1.4_build',
57
+ headers={'User-Agent':'EAOS-setup'})
58
+ with urllib.request.urlopen(request, timeout=60) as response:
59
+ release = json.load(response)
60
+ matches = [a['browser_download_url'] for a in release['assets']
61
+ if a['name'].startswith(GHIDRA+'_') and a['name'].endswith('.zip')]
62
+ if len(matches) != 1:
63
+ raise RuntimeError('Pinned Ghidra release asset is missing or ambiguous')
64
+ return matches[0]
65
+
66
+
67
+ def unpack(archive, tools, directory):
68
+ """Commit a complete extraction; leave an existing runtime untouched."""
69
+ destination = tools/directory
70
+ if destination.exists():
71
+ raise RuntimeError(f'Refusing to replace an existing runtime: {destination}')
72
+ with tempfile.TemporaryDirectory(dir=tools, prefix='.unpack-') as temporary:
73
+ staging = Path(temporary)
74
+ if archive.name.endswith('.zip'):
75
+ subprocess.run(['/usr/bin/ditto','-x','-k',str(archive),str(staging)],check=True)
76
+ else:
77
+ with tarfile.open(archive) as source:
78
+ source.extractall(staging,filter='data')
79
+ extracted = staging/directory
80
+ required = extracted/'Ghidra/application.properties' if directory == GHIDRA else extracted/'Contents/Home/bin/java'
81
+ if not required.is_file():
82
+ raise RuntimeError(f'Archive is missing its expected runtime: {directory}')
83
+ extracted.rename(destination)
84
+
85
+
86
+ def runtime_directory():
87
+ """Installed packages never derive writable storage from site-packages."""
88
+ override = os.environ.get('EAOS_RUNTIME_DIR')
89
+ if override:
90
+ return Path(override).expanduser().resolve()
91
+ source = Path(__file__).resolve()
92
+ checkout = source.parents[2]
93
+ if source.parent.parent.name == 'src' and (checkout/'install.sh').is_file():
94
+ return checkout/'.tools'
95
+ return Path.home()/'Library/Application Support/Every App Is Open Source/runtimes'
96
+
97
+
98
+ def prerequisites():
99
+ if platform.system() != 'Darwin' or platform.machine() != 'arm64':
100
+ raise RuntimeError('Only Apple Silicon macOS is currently supported; no runtime downloads were started.')
101
+ subprocess.run(['/usr/bin/xcode-select','-p'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
102
+ for tool in ['make','clang++']:
103
+ subprocess.run(['/usr/bin/xcrun','--find',tool],stdout=subprocess.DEVNULL,check=True)
104
+
105
+
106
+ def configure():
107
+ tools = runtime_directory()
108
+ ghidra = tools/GHIDRA
109
+ java = tools/JDK/'Contents/Home'
110
+ required = [java/'bin/java', ghidra/'Ghidra/application.properties',
111
+ ghidra/'Ghidra/Features/Decompiler/os/mac_arm_64/decompile']
112
+ if not all(path.is_file() for path in required):
113
+ raise RuntimeError('Java/Ghidra setup is incomplete. Run eaos setup first. Runtime directory: '+str(tools))
114
+ # Match the checkout launcher: prefer our verified managed runtimes over
115
+ # unrelated JAVA_HOME/GHIDRA_INSTALL_DIR values inherited from a client.
116
+ os.environ['JAVA_HOME'] = str(java)
117
+ os.environ['GHIDRA_INSTALL_DIR'] = str(ghidra)
118
+ return tools, ghidra, java
119
+
120
+
121
+ def install(rebuild=False, archive_cache=None):
122
+ prerequisites()
123
+ tools = runtime_directory()
124
+ tools.mkdir(parents=True, exist_ok=True)
125
+ with (tools/'setup.lock').open('a+') as lock:
126
+ try:
127
+ fcntl.flock(lock,fcntl.LOCK_EX | fcntl.LOCK_NB)
128
+ except BlockingIOError:
129
+ raise RuntimeError('Another installation is running in '+str(tools)) from None
130
+ return install_locked(tools, rebuild, archive_cache)
131
+
132
+
133
+ def install_locked(tools, rebuild=False, archive_cache=None):
134
+ pins = json.loads(files('eaos').joinpath('distributions.json').read_text())
135
+ archives = {};downloaded = []
136
+ for name,directory in [('ghidra.zip',GHIDRA),('jdk.tar.gz',JDK)]:
137
+ destination = tools/directory
138
+ required = destination/'Ghidra/application.properties' if directory == GHIDRA else destination/'Contents/Home/bin/java'
139
+ if destination.exists():
140
+ if not required.is_file():
141
+ raise RuntimeError(f'Incomplete runtime at {destination}; move it aside before retrying.')
142
+ continue
143
+ archive = (archive_cache or tools/'distributions')/name
144
+ if not archive.is_file():
145
+ if archive_cache is not None:
146
+ raise RuntimeError(f'Missing archive in supplied cache: {archive}')
147
+ print(f'Downloading pinned {name} ({pins[name]["bytes"]:,} bytes)…',flush=True)
148
+ download(archive_url(name),archive,pins[name]);downloaded.append(name)
149
+ verify_archive(archive,pins[name]);archives[name]=str(archive)
150
+ print(f'Extracting verified {name}…',flush=True)
151
+ unpack(archive,tools,directory)
152
+ ghidra = tools/GHIDRA;java = tools/JDK/'Contents/Home'
153
+ properties = (ghidra/'Ghidra/application.properties').read_text()
154
+ if 'application.version=12.1.4' not in properties.splitlines():
155
+ raise RuntimeError('Unexpected Ghidra version')
156
+ output = ghidra/'Ghidra/Features/Decompiler/os/mac_arm_64/decompile'
157
+ rebuilt = rebuild or not output.is_file()
158
+ if rebuilt:
159
+ print('Building the Apple Silicon decompiler; log: '+str(tools/'decompiler-build.log'),flush=True)
160
+ cpp = ghidra/'Ghidra/Features/Decompiler/src/decompile/cpp'
161
+ # Keep release-generated parsers; no Bison, CMake or other build installs.
162
+ with (tools/'decompiler-build.log').open('w') as log:
163
+ subprocess.run(BUILD_ARGS,cwd=cpp,stdout=log,stderr=subprocess.STDOUT,check=True)
164
+ output.parent.mkdir(parents=True,exist_ok=True)
165
+ temporary = output.with_name('.eaos-decompile')
166
+ try:
167
+ shutil.copy2(cpp/'ghidra_opt',temporary);temporary.replace(output)
168
+ finally:temporary.unlink(missing_ok=True)
169
+ check = subprocess.check_output(['file',str(output)],text=True).strip()
170
+ if 'arm64' not in check:
171
+ raise RuntimeError('Decompiler is not an ARM64 executable: '+check)
172
+ java_check = subprocess.run([str(java/'bin/java'),'-version'],capture_output=True,text=True,check=True)
173
+ if 'version "21.' not in java_check.stderr:
174
+ raise RuntimeError('Unexpected Java version: '+java_check.stderr)
175
+ try:
176
+ pyghidra_version = version('pyghidra')
177
+ except PackageNotFoundError:
178
+ pyghidra_version = None # The checkout installs bindings after native setup.
179
+ report = {'ghidra':str(ghidra),'java_home':str(java),'java_version':java_check.stderr.splitlines()[0],
180
+ 'python':platform.python_version(),'pyghidra':pyghidra_version,
181
+ 'python_executable':sys.executable,'package_location':str(Path(__file__).resolve().parent),
182
+ 'decompiler':check,'decompiler_sha256':digest(output),'decompiler_built':rebuilt,
183
+ 'verified_archives':archives,'downloaded':downloaded}
184
+ temporary = tools/'setup.json.part'
185
+ temporary.write_text(json.dumps(report,indent=2)+'\n')
186
+ temporary.replace(tools/'setup.json')
187
+ print('Ready. Java and Ghidra: '+str(tools),flush=True)
188
+ print('Receipt: '+str(tools/'setup.json'),flush=True)
189
+ return report
eaos/swift.py ADDED
@@ -0,0 +1,20 @@
1
+ """Decode the documented Swift small String bit layout; do not guess pointers."""
2
+
3
+
4
+ def decode_small_string(words):
5
+ if len(words) != 2:
6
+ raise ValueError("Supply two 64-bit words in memory order")
7
+ values = [int(w, 0) if isinstance(w, str) else int(w) for w in words]
8
+ if any(w < 0 or w >= 1 << 64 for w in values):
9
+ raise ValueError("Words must be unsigned 64-bit values")
10
+ raw = b''.join(w.to_bytes(8, 'little') for w in values)
11
+ tag = raw[15]
12
+ if tag & 0xA0 != 0xA0:
13
+ raise ValueError("Not a supported small Swift String discriminator; pointer-backed strings require memory inspection")
14
+ size = tag & 0x0F
15
+ payload = raw[:size]
16
+ text = payload.decode('utf-8', errors='strict')
17
+ if tag & 0x40 and not payload.isascii():
18
+ raise ValueError("ASCII discriminator conflicts with payload")
19
+ return {"text": text, "utf8_bytes": size, "discriminator": hex(tag),
20
+ "raw_hex": raw.hex(), "layout": "Swift small String, 64-bit little endian"}
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: every-app-is-open-source
3
+ Version: 0.1.0
4
+ Summary: Small, evidence-linked reverse-engineering tools for agents
5
+ Author-email: thomasisalwayslate <148886635+thomasisalwayslate@users.noreply.github.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/thomasisalwayslate/every-app-is-open-source
8
+ Requires-Python: <3.14,>=3.13
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Requires-Dist: pyghidra==3.1.0
13
+ Requires-Dist: jpype1==1.5.2
14
+ Requires-Dist: packaging==26.3
15
+ Dynamic: license-file
16
+
17
+ # Every App Is Open Source
18
+
19
+ **Same answers as REA. 36% cheaper. 27% faster.**
20
+
21
+ I built this to give an AI agent the tools to take a compiled program apart and work out what it does, without the source code. Ghidra does the actual decompiling. My part is everything around it: a small set of tools, short replies by default, honest labels on every answer, and evidence you can trace back to the binary.
22
+
23
+ Then I tested it against [REA](https://github.com/morluto/rea), the 300,000-star tool everyone's using, and against the community [GhidraMCP](https://github.com/LaurieWired/GhidraMCP). On a secretly modified Doom, all three got every answer right. Mine was the cheapest and the fastest.
24
+
25
+ > The name is a provocation, not a license. This shows you how an app works. It doesn't give you the right to copy it.
26
+
27
+ ## Results
28
+
29
+ Same model for everyone (gpt-6.1-sol, high effort), fresh sessions, six questions, three runs per tool:
30
+
31
+ | Tool | Correct | Median time per run | Tokens, 3 runs | API-equivalent cost, 3 runs |
32
+ |---|---:|---:|---:|---:|
33
+ | **Every App Is Open Source** | **18/18** | **121.8s** | **1,728,000** | **$0.70** |
34
+ | Community GhidraMCP | 18/18 | 146.2s | 2,337,886 | $0.84 |
35
+ | REA 6.7.0 + its bundled skill | 18/18 | 166.7s | 3,603,877 | $1.09 |
36
+
37
+ **How I kept it honest:**
38
+ - **Doom's answers are all over the internet, so I changed them.** I built Chocolate Doom with eight gameplay rules secretly altered and stripped its function names. An agent answering from memory gets the original values, and those are now wrong.
39
+ - **I froze the questions and the answer key before the first run.** The answer key comes from running the modified game code, not from me reading it.
40
+ - **Everyone got the same head start.** All three began with the program already analyzed, outside the timer.
41
+ - **REA ran the way people actually install it,** with its bundled skill and its full tool catalog.
42
+
43
+ **What this doesn't prove:** all three scored 100%, so mine isn't smarter, just cheaper and faster. It's one game, three runs per tool, compiled programs only.
44
+
45
+ My first version actually lost to GhidraMCP on cost. I read the logs, cut the repeated metadata and the detail nobody had asked for yet, then retested on new questions and a new build. Everything is kept, including the round I lost.
46
+
47
+ [Full results](reports/round2/results.md) · [Every run](reports/round2/results.json) · [Answer-by-answer check](reports/round2/answer-review.json) · [Frozen protocol](benchmark/protocol-round2.json) · [The round I lost](reports/results.md)
48
+
49
+ ## Install
50
+
51
+ You need an Apple Silicon Mac, [uv](https://docs.astral.sh/uv/) and Apple's Command Line Tools (`xcode-select --install`).
52
+
53
+ ```sh
54
+ uvx --python 3.13 --from every-app-is-open-source eaos setup
55
+ ```
56
+
57
+ That downloads checksum-pinned Ghidra 12.1.4 and Java 21, and builds Ghidra's Apple Silicon decompiler, because the official download doesn't include one. Everything lands in `~/Library/Application Support/Every App Is Open Source/`. Nothing global, no shell profile edits, no touching your agent's settings. It takes under a minute, and running it again reuses what's there.
58
+
59
+ ## Use it
60
+
61
+ Point your agent at one program:
62
+
63
+ ```sh
64
+ uvx --python 3.13 --from every-app-is-open-source eaos mcp /absolute/path/to/program
65
+ ```
66
+
67
+ For an MCP client config, the command is `uvx` and the arguments are:
68
+
69
+ ```json
70
+ ["--python", "3.13", "--from", "every-app-is-open-source", "eaos", "mcp", "/absolute/path/to/program"]
71
+ ```
72
+
73
+ Or try a single tool from the terminal:
74
+
75
+ ```sh
76
+ uvx --python 3.13 --from every-app-is-open-source eaos call /absolute/path/to/program open_program
77
+ ```
78
+
79
+ The server only sees the one program you give it. It can't read other files on your machine.
80
+
81
+ ## The seven tools
82
+
83
+ I counted every tool call the agents made across my earlier 24 test runs and built only what they actually used.
84
+
85
+ | Tool | What it does |
86
+ |---|---|
87
+ | `open_program` | What the program is: architecture, memory layout, analysis status |
88
+ | `search` | Find text inside the program and where it's used, or find functions |
89
+ | `inspect` | Decompiled code, raw instructions, callers and callees |
90
+ | `read_memory` | Read the actual bytes at an address |
91
+ | `calculate` | Safe arithmetic and byte decoding, so the agent doesn't do math in its head |
92
+ | `decode_swift_string` | Recover text hidden inside Swift code on Apple platforms |
93
+ | `read_evidence` | Pull up the full saved evidence behind any reply, hash-checked |
94
+
95
+ ## Never "exact" for a guess
96
+
97
+ That was REA's worst habit: a name-based guess labelled "observed, exact." Here, every answer says what it is:
98
+
99
+ - **fact**: read straight from the binary
100
+ - **computed**: worked out deterministically, like decompiled code or a decoded string
101
+ - **guess**: an interpretation with nothing solid behind it
102
+
103
+ Decompiled code is computed, not the original source. When a tool fails, it says it failed.
104
+
105
+ ## Analyze once, reuse forever
106
+
107
+ Ghidra's analysis is slow, so each program gets its own `eaos-analysis/<sha256>/` folder right next to it, holding the analyzed project and every piece of evidence. Opening the same program again reuses all of it.
108
+
109
+ One Ghidra quirk: it won't create projects inside a folder whose name starts with a dot, like `.cache`. Keep the program you're analyzing out of those.
110
+
111
+ ## From a checkout
112
+
113
+ If you'd rather run it from the repo:
114
+
115
+ ```sh
116
+ ./install.sh
117
+ ./eaos mcp /absolute/path/to/program
118
+ ```
119
+
120
+ Same requirements. Java, Ghidra and Python all install inside the checkout.
121
+
122
+ ## What's next
123
+
124
+ I only claim "better" where I've measured it:
125
+
126
+ | Area | Status |
127
+ |---|---|
128
+ | Compiled programs | ✅ Beat REA and GhidraMCP on cost and speed, on modified Doom |
129
+ | JavaScript / Electron apps | Next: VS Code, with its own hidden answer key |
130
+ | Android apps | Planned |
131
+ | .NET apps | Planned |
132
+ | Websites, watching apps run | Planned |
133
+
134
+ Each one gets the same treatment: a secretly modified target, a frozen answer key and a fair fight against REA, before I claim anything.
135
+
136
+ ## Rerun my test
137
+
138
+ ```sh
139
+ uv run python -m unittest discover -s tests -v
140
+ python3 benchmark/build_doom.py
141
+ ```
142
+
143
+ `build_doom.py` makes a fresh build with new random changes, so you get your own answer key. The binary and answer key from my run stay private, since publishing them would spoil the test. In the saved reports, file paths from my machine are shortened to `<repo>`.
144
+
145
+ ## Credits
146
+
147
+ Ghidra and PyGhidra do the heavy lifting; they're the work of their own contributors. The comparison baseline is LaurieWired's GhidraMCP. The test target is Chocolate Doom, based on id Software's Doom and released under GPLv2.
148
+
149
+ ## License
150
+
151
+ [Apache 2.0](LICENSE). If you build on this, keep the [NOTICE](NOTICE) file. Before you point it at anything, read the [disclaimer](DISCLAIMER.md).
@@ -0,0 +1,15 @@
1
+ eaos/__init__.py,sha256=KGP0y-awfQFFRKsNDjRDRGBTKLr1nC4hSmjzolQ91QM,89
2
+ eaos/backend.py,sha256=q--HSnl_tQN1nmn3izeuksrKzvi4OwvdJkti0eWL-aE,13477
3
+ eaos/calculator.py,sha256=jvktzy_TOo0edUHn04Z7dVNK5M0GxWGc-2MbJ6xivso,2529
4
+ eaos/cli.py,sha256=I3AnfxMdTUKR8pbzpCv7C80r4Txk5kRXAm6rDYpCWtY,5591
5
+ eaos/distributions.json,sha256=D17G5md2pnh3mPHKhV6ZH0YBZ-IfBEgHLvqGIzVI7tk,258
6
+ eaos/evidence.py,sha256=do29Hv-g0-YAR0oC2HhyxwPt4hWKM7UyVSLX4ZTEoSE,2448
7
+ eaos/runtime.py,sha256=QKWcUzC-7YQmoogZfhK1eDqUfgbJbvN7U58BJjiPM90,9167
8
+ eaos/swift.py,sha256=9xfXN5NX7M2EeuYy0QnFOa9ffmm_6EXg7ZhdplahCzY,1006
9
+ every_app_is_open_source-0.1.0.dist-info/licenses/LICENSE,sha256=yVuuHRzgI17MzTVgt3LsHvuX80innw--CmNPDCzO_iw,11358
10
+ every_app_is_open_source-0.1.0.dist-info/licenses/NOTICE,sha256=JDnaDWl6m2Cha5CvOyqRBuy0fXz3flC2iQ86Hk_HQ0w,324
11
+ every_app_is_open_source-0.1.0.dist-info/METADATA,sha256=QkGqKaGvuNI1RXRqfcMawFHnvSnUCFcDtid0xbbvE6c,7280
12
+ every_app_is_open_source-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ every_app_is_open_source-0.1.0.dist-info/entry_points.txt,sha256=munXUOz9BrB9TnziGx42RQB1XH1MVd2SDdzaiduW_uM,39
14
+ every_app_is_open_source-0.1.0.dist-info/top_level.txt,sha256=TINcf2knZ4iq3rc398i6PR8SetMMRVJDKQaoXPFd6Ck,5
15
+ every_app_is_open_source-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eaos = eaos.cli:main
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
@@ -0,0 +1,7 @@
1
+ Every App Is Open Source
2
+ Copyright 2026 thomasisalwayslate
3
+ https://github.com/thomasisalwayslate/every-app-is-open-source
4
+
5
+ This product includes software developed by thomasisalwayslate.
6
+ If you redistribute this software or build on it, keep this NOTICE file,
7
+ as required by section 4(d) of the Apache License, Version 2.0.