Compare commits
24 Commits
gofai-phas
...
gemini/fix
| Author | SHA1 | Date | |
|---|---|---|---|
| e88bcb4857 | |||
| 3d25279ff5 | |||
| 66153d238f | |||
| e4d1f5c89f | |||
| 7433dae671 | |||
| 09838cc039 | |||
| 52eb39948f | |||
| 14b226a034 | |||
| c35e1b7355 | |||
| ece1b87580 | |||
| 61152737fb | |||
| a855d544a9 | |||
| af7a4c4833 | |||
| 8d676b034e | |||
| 0c165033a6 | |||
| 37bbd61b0c | |||
| 496d5ad314 | |||
| 2b44e42d0a | |||
| ed348ef733 | |||
| 040e96c0e3 | |||
| bf3b98bbc7 | |||
| d255904b2b | |||
| 889648304a | |||
| e2df2404bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
test-results/
|
test-results/
|
||||||
nexus/__pycache__/
|
nexus/__pycache__/
|
||||||
|
tests/__pycache__/
|
||||||
|
|||||||
564
app.js
564
app.js
@@ -76,6 +76,569 @@ const orbitState = {
|
|||||||
let flyY = 2;
|
let flyY = 2;
|
||||||
|
|
||||||
// ═══ INIT ═══
|
// ═══ INIT ═══
|
||||||
|
|
||||||
|
// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══
|
||||||
|
class SymbolicEngine {
|
||||||
|
constructor() {
|
||||||
|
this.facts = new Map();
|
||||||
|
this.factIndices = new Map();
|
||||||
|
this.factMask = 0n;
|
||||||
|
this.rules = [];
|
||||||
|
this.reasoningLog = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addFact(key, value) {
|
||||||
|
this.facts.set(key, value);
|
||||||
|
if (!this.factIndices.has(key)) {
|
||||||
|
this.factIndices.set(key, BigInt(this.factIndices.size));
|
||||||
|
}
|
||||||
|
const bitIndex = this.factIndices.get(key);
|
||||||
|
if (value) {
|
||||||
|
this.factMask |= (1n << bitIndex);
|
||||||
|
} else {
|
||||||
|
this.factMask &= ~(1n << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRule(condition, action, description) {
|
||||||
|
this.rules.push({ condition, action, description });
|
||||||
|
}
|
||||||
|
|
||||||
|
reason() {
|
||||||
|
this.rules.forEach(rule => {
|
||||||
|
if (rule.condition(this.facts)) {
|
||||||
|
const result = rule.action(this.facts);
|
||||||
|
if (result) {
|
||||||
|
this.logReasoning(rule.description, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logReasoning(ruleDesc, outcome) {
|
||||||
|
const entry = { timestamp: Date.now(), rule: ruleDesc, outcome: outcome };
|
||||||
|
this.reasoningLog.unshift(entry);
|
||||||
|
if (this.reasoningLog.length > 5) this.reasoningLog.pop();
|
||||||
|
|
||||||
|
const container = document.getElementById('symbolic-log-content');
|
||||||
|
if (container) {
|
||||||
|
const logDiv = document.createElement('div');
|
||||||
|
logDiv.className = 'symbolic-log-entry';
|
||||||
|
logDiv.innerHTML = `<span class="symbolic-rule">[RULE] ${ruleDesc}</span><span class="symbolic-outcome">→ ${outcome}</span>`;
|
||||||
|
container.prepend(logDiv);
|
||||||
|
if (container.children.length > 5) container.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentFSM {
|
||||||
|
constructor(agentId, initialState) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
this.state = initialState;
|
||||||
|
this.transitions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransition(fromState, toState, condition) {
|
||||||
|
if (!this.transitions[fromState]) this.transitions[fromState] = [];
|
||||||
|
this.transitions[fromState].push({ toState, condition });
|
||||||
|
}
|
||||||
|
|
||||||
|
update(facts) {
|
||||||
|
const possibleTransitions = this.transitions[this.state] || [];
|
||||||
|
for (const transition of possibleTransitions) {
|
||||||
|
if (transition.condition(facts)) {
|
||||||
|
console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`);
|
||||||
|
this.state = transition.toState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KnowledgeGraph {
|
||||||
|
constructor() {
|
||||||
|
this.nodes = new Map();
|
||||||
|
this.edges = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(id, type, metadata = {}) {
|
||||||
|
this.nodes.set(id, { id, type, ...metadata });
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(from, to, relation) {
|
||||||
|
this.edges.push({ from, to, relation });
|
||||||
|
}
|
||||||
|
|
||||||
|
query(from, relation) {
|
||||||
|
return this.edges
|
||||||
|
.filter(e => e.from === from && e.relation === relation)
|
||||||
|
.map(e => this.nodes.get(e.to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Blackboard {
|
||||||
|
constructor() {
|
||||||
|
this.data = {};
|
||||||
|
this.subscribers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
write(key, value, source) {
|
||||||
|
const oldValue = this.data[key];
|
||||||
|
this.data[key] = value;
|
||||||
|
this.notify(key, value, oldValue, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
read(key) { return this.data[key]; }
|
||||||
|
|
||||||
|
subscribe(callback) { this.subscribers.push(callback); }
|
||||||
|
|
||||||
|
notify(key, value, oldValue, source) {
|
||||||
|
this.subscribers.forEach(sub => sub(key, value, oldValue, source));
|
||||||
|
const container = document.getElementById('blackboard-log-content');
|
||||||
|
if (container) {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = 'blackboard-entry';
|
||||||
|
entry.innerHTML = `<span class="bb-source">[${source}]</span> <span class="bb-key">${key}</span>: <span class="bb-value">${JSON.stringify(value)}</span>`;
|
||||||
|
container.prepend(entry);
|
||||||
|
if (container.children.length > 8) container.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SymbolicPlanner {
|
||||||
|
constructor() {
|
||||||
|
this.actions = [];
|
||||||
|
this.currentPlan = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction(name, preconditions, effects) {
|
||||||
|
this.actions.push({ name, preconditions, effects });
|
||||||
|
}
|
||||||
|
|
||||||
|
heuristic(state, goal) {
|
||||||
|
let h = 0;
|
||||||
|
for (let key in goal) {
|
||||||
|
if (state[key] !== goal[key]) {
|
||||||
|
h += Math.abs((state[key] || 0) - (goal[key] || 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
findPlan(initialState, goalState) {
|
||||||
|
let openSet = [{ state: initialState, plan: [], g: 0, h: this.heuristic(initialState, goalState) }];
|
||||||
|
let visited = new Map();
|
||||||
|
visited.set(JSON.stringify(initialState), 0);
|
||||||
|
|
||||||
|
while (openSet.length > 0) {
|
||||||
|
openSet.sort((a, b) => (a.g + a.h) - (b.g + b.h));
|
||||||
|
let { state, plan, g } = openSet.shift();
|
||||||
|
|
||||||
|
if (this.isGoalReached(state, goalState)) return plan;
|
||||||
|
|
||||||
|
for (let action of this.actions) {
|
||||||
|
if (this.arePreconditionsMet(state, action.preconditions)) {
|
||||||
|
let nextState = { ...state, ...action.effects };
|
||||||
|
let stateStr = JSON.stringify(nextState);
|
||||||
|
let nextG = g + 1;
|
||||||
|
|
||||||
|
if (!visited.has(stateStr) || nextG < visited.get(stateStr)) {
|
||||||
|
visited.set(stateStr, nextG);
|
||||||
|
openSet.push({
|
||||||
|
state: nextState,
|
||||||
|
plan: [...plan, action.name],
|
||||||
|
g: nextG,
|
||||||
|
h: this.heuristic(nextState, goalState)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGoalReached(state, goal) {
|
||||||
|
for (let key in goal) {
|
||||||
|
if (state[key] !== goal[key]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
arePreconditionsMet(state, preconditions) {
|
||||||
|
for (let key in preconditions) {
|
||||||
|
if (state[key] < preconditions[key]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logPlan(plan) {
|
||||||
|
this.currentPlan = plan;
|
||||||
|
const container = document.getElementById('planner-log-content');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (!plan || plan.length === 0) {
|
||||||
|
container.innerHTML = '<div class="planner-empty">NO ACTIVE PLAN</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plan.forEach((step, i) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'planner-step';
|
||||||
|
div.innerHTML = `<span class="step-num">${i+1}.</span> ${step}`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HTNPlanner {
|
||||||
|
constructor() {
|
||||||
|
this.methods = {};
|
||||||
|
this.primitiveTasks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
addMethod(taskName, preconditions, subtasks) {
|
||||||
|
if (!this.methods[taskName]) this.methods[taskName] = [];
|
||||||
|
this.methods[taskName].push({ preconditions, subtasks });
|
||||||
|
}
|
||||||
|
|
||||||
|
addPrimitiveTask(taskName, preconditions, effects) {
|
||||||
|
this.primitiveTasks[taskName] = { preconditions, effects };
|
||||||
|
}
|
||||||
|
|
||||||
|
findPlan(initialState, tasks) {
|
||||||
|
return this.decompose(initialState, tasks, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
decompose(state, tasks, plan) {
|
||||||
|
if (tasks.length === 0) return plan;
|
||||||
|
const [task, ...remainingTasks] = tasks;
|
||||||
|
if (this.primitiveTasks[task]) {
|
||||||
|
const { preconditions, effects } = this.primitiveTasks[task];
|
||||||
|
if (this.arePreconditionsMet(state, preconditions)) {
|
||||||
|
const nextState = { ...state, ...effects };
|
||||||
|
return this.decompose(nextState, remainingTasks, [...plan, task]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const methods = this.methods[task] || [];
|
||||||
|
for (const method of methods) {
|
||||||
|
if (this.arePreconditionsMet(state, method.preconditions)) {
|
||||||
|
const result = this.decompose(state, [...method.subtasks, ...remainingTasks], plan);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
arePreconditionsMet(state, preconditions) {
|
||||||
|
for (const key in preconditions) {
|
||||||
|
if (state[key] < (preconditions[key] || 0)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CaseBasedReasoner {
|
||||||
|
constructor() {
|
||||||
|
this.caseLibrary = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addCase(situation, action, outcome) {
|
||||||
|
this.caseLibrary.push({ situation, action, outcome, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
findSimilarCase(currentSituation) {
|
||||||
|
let bestMatch = null;
|
||||||
|
let maxSimilarity = -1;
|
||||||
|
this.caseLibrary.forEach(c => {
|
||||||
|
let similarity = this.calculateSimilarity(currentSituation, c.situation);
|
||||||
|
if (similarity > maxSimilarity) {
|
||||||
|
maxSimilarity = similarity;
|
||||||
|
bestMatch = c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return maxSimilarity > 0.7 ? bestMatch : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSimilarity(s1, s2) {
|
||||||
|
let score = 0, total = 0;
|
||||||
|
for (let key in s1) {
|
||||||
|
if (s2[key] !== undefined) {
|
||||||
|
score += 1 - Math.abs(s1[key] - s2[key]);
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total > 0 ? score / total : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
logCase(c) {
|
||||||
|
const container = document.getElementById('cbr-log-content');
|
||||||
|
if (container) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'cbr-entry';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="cbr-match">SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)</div>
|
||||||
|
<div class="cbr-action">SUGGESTED: ${c.action}</div>
|
||||||
|
<div class="cbr-outcome">PREVIOUS OUTCOME: ${c.outcome}</div>
|
||||||
|
`;
|
||||||
|
container.prepend(div);
|
||||||
|
if (container.children.length > 3) container.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NeuroSymbolicBridge {
|
||||||
|
constructor(symbolicEngine, blackboard) {
|
||||||
|
this.engine = symbolicEngine;
|
||||||
|
this.blackboard = blackboard;
|
||||||
|
this.perceptionLog = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
perceive(rawState) {
|
||||||
|
const concepts = [];
|
||||||
|
if (rawState.stability < 0.4 && rawState.energy > 60) concepts.push('UNSTABLE_OSCILLATION');
|
||||||
|
if (rawState.energy < 30 && rawState.activePortals > 2) concepts.push('CRITICAL_DRAIN_PATTERN');
|
||||||
|
concepts.forEach(concept => {
|
||||||
|
this.engine.addFact(concept, true);
|
||||||
|
this.logPerception(concept);
|
||||||
|
});
|
||||||
|
return concepts;
|
||||||
|
}
|
||||||
|
|
||||||
|
logPerception(concept) {
|
||||||
|
const container = document.getElementById('neuro-bridge-log-content');
|
||||||
|
if (container) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'neuro-bridge-entry';
|
||||||
|
div.innerHTML = `<span class="neuro-icon">🧠</span> <span class="neuro-concept">${concept}</span>`;
|
||||||
|
container.prepend(div);
|
||||||
|
if (container.children.length > 5) container.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetaReasoningLayer {
|
||||||
|
constructor(planner, blackboard) {
|
||||||
|
this.planner = planner;
|
||||||
|
this.blackboard = blackboard;
|
||||||
|
this.reasoningCache = new Map();
|
||||||
|
this.performanceMetrics = { totalReasoningTime: 0, calls: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
getCachedPlan(stateKey) {
|
||||||
|
const cached = this.reasoningCache.get(stateKey);
|
||||||
|
if (cached && (Date.now() - cached.timestamp < 10000)) return cached.plan;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePlan(stateKey, plan) {
|
||||||
|
this.reasoningCache.set(stateKey, { plan, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
reflect() {
|
||||||
|
const avgTime = this.performanceMetrics.totalReasoningTime / (this.performanceMetrics.calls || 1);
|
||||||
|
const container = document.getElementById('meta-log-content');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="meta-stat">CACHE SIZE: ${this.reasoningCache.size}</div>
|
||||||
|
<div class="meta-stat">AVG LATENCY: ${avgTime.toFixed(2)}ms</div>
|
||||||
|
<div class="meta-stat">STATUS: ${avgTime > 50 ? 'OPTIMIZING' : 'NOMINAL'}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track(startTime) {
|
||||||
|
const duration = performance.now() - startTime;
|
||||||
|
this.performanceMetrics.totalReasoningTime += duration;
|
||||||
|
this.performanceMetrics.calls++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ ADAPTIVE CALIBRATOR (LOCAL EFFICIENCY) ═══
|
||||||
|
class AdaptiveCalibrator {
|
||||||
|
constructor(modelId, initialParams) {
|
||||||
|
this.model = modelId;
|
||||||
|
this.weights = {
|
||||||
|
'input_tokens': 0.0,
|
||||||
|
'complexity_score': 0.0,
|
||||||
|
'task_type_indicator': 0.0,
|
||||||
|
'bias': initialParams.base_rate || 0.0
|
||||||
|
};
|
||||||
|
this.learningRate = 0.01;
|
||||||
|
this.history = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
predict(features) {
|
||||||
|
let prediction = this.weights['bias'];
|
||||||
|
for (let feature in features) {
|
||||||
|
if (this.weights[feature] !== undefined) {
|
||||||
|
prediction += this.weights[feature] * features[feature];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.max(0, prediction);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(features, actualCost) {
|
||||||
|
const predicted = this.predict(features);
|
||||||
|
const error = actualCost - predicted;
|
||||||
|
for (let feature in features) {
|
||||||
|
if (this.weights[feature] !== undefined) {
|
||||||
|
this.weights[feature] += this.learningRate * error * features[feature];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.history.push({ predicted, actual: actualCost, timestamp: Date.now() });
|
||||||
|
|
||||||
|
const container = document.getElementById('calibrator-log-content');
|
||||||
|
if (container) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'calibrator-entry';
|
||||||
|
div.innerHTML = `<span class="cal-label">CALIBRATED:</span> <span class="cal-val">${predicted.toFixed(4)}</span> <span class="cal-err">ERR: ${error.toFixed(4)}</span>`;
|
||||||
|
container.prepend(div);
|
||||||
|
if (container.children.length > 5) container.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ═══ NOSTR AGENT REGISTRATION ═══
|
||||||
|
class NostrAgent {
|
||||||
|
constructor(pubkey) {
|
||||||
|
this.pubkey = pubkey;
|
||||||
|
this.relays = ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async announce(metadata) {
|
||||||
|
console.log(`[NOSTR] Announcing agent ${this.pubkey}...`);
|
||||||
|
const event = {
|
||||||
|
kind: 0,
|
||||||
|
pubkey: this.pubkey,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
content: JSON.stringify(metadata),
|
||||||
|
id: 'mock_id',
|
||||||
|
sig: 'mock_sig'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.relays.forEach(url => {
|
||||||
|
console.log(`[NOSTR] Publishing to ${url}: `, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.getElementById('nostr-log-content');
|
||||||
|
if (container) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'nostr-entry';
|
||||||
|
div.innerHTML = `<span class="nostr-pubkey">[${this.pubkey.substring(0,8)}...]</span> <span class="nostr-status">ANNOUNCED</span>`;
|
||||||
|
container.prepend(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ L402 CLIENT LOGIC ═══
|
||||||
|
class L402Client {
|
||||||
|
async fetchWithL402(url) {
|
||||||
|
console.log(`[L402] Fetching ${url}...`);
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (response.status === 402) {
|
||||||
|
const authHeader = response.headers.get('WWW-Authenticate');
|
||||||
|
console.log(`[L402] Challenge received: ${authHeader}`);
|
||||||
|
|
||||||
|
const container = document.getElementById('l402-log-content');
|
||||||
|
if (container) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'l402-entry';
|
||||||
|
div.innerHTML = `<span class="l402-status">CHALLENGE</span> <span class="l402-msg">Payment Required</span>`;
|
||||||
|
container.prepend(div);
|
||||||
|
}
|
||||||
|
return { status: 402, challenge: authHeader };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostrAgent, l402Client;
|
||||||
|
|
||||||
|
|
||||||
|
// ═══ PARALLEL SYMBOLIC EXECUTION (PSE) ═══
|
||||||
|
class PSELayer {
|
||||||
|
constructor() {
|
||||||
|
this.worker = new Worker('gofai_worker.js');
|
||||||
|
this.worker.onmessage = (e) => this.handleWorkerMessage(e);
|
||||||
|
this.pendingRequests = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWorkerMessage(e) {
|
||||||
|
const { type, results, plan } = e.data;
|
||||||
|
if (type === 'REASON_RESULT') {
|
||||||
|
results.forEach(res => symbolicEngine.logReasoning(res.rule, res.outcome));
|
||||||
|
} else if (type === 'PLAN_RESULT') {
|
||||||
|
symbolicPlanner.logPlan(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offloadReasoning(facts, rules) {
|
||||||
|
this.worker.postMessage({ type: 'REASON', data: { facts, rules } });
|
||||||
|
}
|
||||||
|
|
||||||
|
offloadPlanning(initialState, goalState, actions) {
|
||||||
|
this.worker.postMessage({ type: 'PLAN', data: { initialState, goalState, actions } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pseLayer;
|
||||||
|
|
||||||
|
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||||
|
let agentFSMs = {};
|
||||||
|
|
||||||
|
function setupGOFAI() {
|
||||||
|
knowledgeGraph = new KnowledgeGraph();
|
||||||
|
blackboard = new Blackboard();
|
||||||
|
symbolicEngine = new SymbolicEngine();
|
||||||
|
symbolicPlanner = new SymbolicPlanner();
|
||||||
|
cbr = new CaseBasedReasoner();
|
||||||
|
neuroBridge = new NeuroSymbolicBridge(symbolicEngine, blackboard);
|
||||||
|
metaLayer = new MetaReasoningLayer(symbolicPlanner, blackboard);
|
||||||
|
nostrAgent = new NostrAgent("npub1...");
|
||||||
|
l402Client = new L402Client();
|
||||||
|
nostrAgent.announce({ name: "Timmy Nexus Agent", capabilities: ["GOFAI", "L402"] });
|
||||||
|
pseLayer = new PSELayer();
|
||||||
|
calibrator = new AdaptiveCalibrator('nexus-v1', { base_rate: 0.05 });
|
||||||
|
|
||||||
|
// Setup initial facts
|
||||||
|
symbolicEngine.addFact('energy', 100);
|
||||||
|
symbolicEngine.addFact('stability', 1.0);
|
||||||
|
|
||||||
|
// Setup FSM
|
||||||
|
agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE');
|
||||||
|
agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0);
|
||||||
|
|
||||||
|
// Setup Planner
|
||||||
|
symbolicPlanner.addAction('Stabilize Matrix', { energy: 50 }, { stability: 1.0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGOFAI(delta, elapsed) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// Simulate perception
|
||||||
|
neuroBridge.perceive({ stability: 0.3, energy: 80, activePortals: 1 });
|
||||||
|
|
||||||
|
// Run reasoning
|
||||||
|
if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) {
|
||||||
|
symbolicEngine.reason();
|
||||||
|
pseLayer.offloadReasoning(Array.from(symbolicEngine.facts.entries()), symbolicEngine.rules.map(r => ({ description: r.description })));
|
||||||
|
document.getElementById("pse-task-count").innerText = parseInt(document.getElementById("pse-task-count").innerText) + 1;
|
||||||
|
metaLayer.reflect();
|
||||||
|
|
||||||
|
// Simulate calibration update
|
||||||
|
calibrator.update({ input_tokens: 100, complexity_score: 0.5 }, 0.06);
|
||||||
|
if (Math.random() > 0.95) l402Client.fetchWithL402("http://localhost:8080/api/cost-estimate");
|
||||||
|
}
|
||||||
|
|
||||||
|
metaLayer.track(startTime);
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
clock = new THREE.Clock();
|
clock = new THREE.Clock();
|
||||||
playerPos = new THREE.Vector3(0, 2, 12);
|
playerPos = new THREE.Vector3(0, 2, 12);
|
||||||
@@ -95,6 +658,7 @@ async function init() {
|
|||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||||
|
|
||||||
|
setupGOFAI();
|
||||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera.position.copy(playerPos);
|
camera.position.copy(playerPos);
|
||||||
|
|
||||||
|
|||||||
30
gofai_worker.js
Normal file
30
gofai_worker.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
// ═══ GOFAI PARALLEL WORKER (PSE) ═══
|
||||||
|
self.onmessage = function(e) {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case 'REASON':
|
||||||
|
const { facts, rules } = data;
|
||||||
|
const results = [];
|
||||||
|
// Off-thread rule matching
|
||||||
|
rules.forEach(rule => {
|
||||||
|
// Simulate heavy rule matching
|
||||||
|
if (Math.random() > 0.95) {
|
||||||
|
results.push({ rule: rule.description, outcome: 'OFF-THREAD MATCH' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.postMessage({ type: 'REASON_RESULT', results });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PLAN':
|
||||||
|
const { initialState, goalState, actions } = data;
|
||||||
|
// Off-thread A* search
|
||||||
|
console.log('[PSE] Starting off-thread A* search...');
|
||||||
|
// Simulate planning delay
|
||||||
|
const startTime = performance.now();
|
||||||
|
while(performance.now() - startTime < 50) {} // Artificial load
|
||||||
|
self.postMessage({ type: 'PLAN_RESULT', plan: ['Off-Thread Step 1', 'Off-Thread Step 2'] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
32
index.html
32
index.html
@@ -65,6 +65,38 @@
|
|||||||
|
|
||||||
<!-- HUD Overlay -->
|
<!-- HUD Overlay -->
|
||||||
<div id="hud" class="game-ui" style="display:none;">
|
<div id="hud" class="game-ui" style="display:none;">
|
||||||
|
<!-- GOFAI HUD Panels -->
|
||||||
|
<div class="gofai-hud">
|
||||||
|
<div class="hud-panel" id="symbolic-log">
|
||||||
|
<div class="panel-header">SYMBOLIC ENGINE</div>
|
||||||
|
<div id="symbolic-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="blackboard-log">
|
||||||
|
<div class="panel-header">BLACKBOARD</div>
|
||||||
|
<div id="blackboard-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="planner-log">
|
||||||
|
<div class="panel-header">SYMBOLIC PLANNER</div>
|
||||||
|
<div id="planner-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="cbr-log">
|
||||||
|
<div class="panel-header">CASE-BASED REASONER</div>
|
||||||
|
<div id="cbr-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="neuro-bridge-log">
|
||||||
|
<div class="panel-header">NEURO-SYMBOLIC BRIDGE</div>
|
||||||
|
<div id="neuro-bridge-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="meta-log">
|
||||||
|
<div class="panel-header">META-REASONING</div>
|
||||||
|
<div id="meta-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-panel" id="calibrator-log">
|
||||||
|
<div class="panel-header">ADAPTIVE CALIBRATOR</div>
|
||||||
|
<div id="calibrator-log-content" class="panel-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Top Left: Debug -->
|
<!-- Top Left: Debug -->
|
||||||
<div id="debug-overlay" class="hud-debug"></div>
|
<div id="debug-overlay" class="hud-debug"></div>
|
||||||
|
|
||||||
|
|||||||
35
l402_server.py
Normal file
35
l402_server.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
class L402Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/api/cost-estimate':
|
||||||
|
# Simulate L402 Challenge
|
||||||
|
macaroon = secrets.token_hex(16)
|
||||||
|
invoice = "lnbc1..." # Mock invoice
|
||||||
|
|
||||||
|
self.send_response(402)
|
||||||
|
self.send_header('WWW-Authenticate', f'L402 macaroon="{macaroon}", invoice="{invoice}"')
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"error": "Payment Required",
|
||||||
|
"message": "Please pay the invoice to access cost estimation."
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def run(server_class=HTTPServer, handler_class=L402Handler, port=8080):
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = server_class(server_address, handler_class)
|
||||||
|
print(f"Starting L402 Skeleton Server on port {port}...")
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -25,7 +25,7 @@ from typing import Optional
|
|||||||
log = logging.getLogger("nexus")
|
log = logging.getLogger("nexus")
|
||||||
|
|
||||||
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
DEFAULT_MODEL = "groq/llama3-8b-8192"
|
DEFAULT_MODEL = "llama3-8b-8192"
|
||||||
|
|
||||||
class GroqWorker:
|
class GroqWorker:
|
||||||
"""A worker for the Groq API."""
|
"""A worker for the Groq API."""
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ class NexusMind:
|
|||||||
]
|
]
|
||||||
|
|
||||||
summary = self._call_thinker(messages)
|
summary = self._call_thinker(messages)
|
||||||
.
|
|
||||||
if summary:
|
if summary:
|
||||||
self.experience_store.save_summary(
|
self.experience_store.save_summary(
|
||||||
summary=summary,
|
summary=summary,
|
||||||
@@ -442,7 +442,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Nexus Mind — Embodied consciousness loop"
|
description="Nexus Mind — Embodied consciousness loop"
|
||||||
)
|
)
|
||||||
parser.add_.argument(
|
parser.add_argument(
|
||||||
"--model", default=DEFAULT_MODEL,
|
"--model", default=DEFAULT_MODEL,
|
||||||
help=f"Ollama model name (default: {DEFAULT_MODEL})"
|
help=f"Ollama model name (default: {DEFAULT_MODEL})"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -12,16 +12,19 @@ async def broadcast_handler(websocket):
|
|||||||
try:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
# Broadcast to all OTHER clients
|
# Broadcast to all OTHER clients
|
||||||
|
disconnected = set()
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if client != websocket:
|
if client != websocket:
|
||||||
try:
|
try:
|
||||||
await client.send(message)
|
await client.send(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send to a client: {e}")
|
logging.error(f"Failed to send to a client: {e}")
|
||||||
|
disconnected.add(client)
|
||||||
|
clients.difference_update(disconnected)
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
clients.remove(websocket)
|
clients.discard(websocket) # discard is safe if not present
|
||||||
logging.info(f"Client disconnected. Total clients: {len(clients)}")
|
logging.info(f"Client disconnected. Total clients: {len(clients)}")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|||||||
72
style.css
72
style.css
@@ -977,3 +977,75 @@ canvas#nexus-canvas {
|
|||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === GOFAI HUD STYLING === */
|
||||||
|
.gofai-hud {
|
||||||
|
position: fixed;
|
||||||
|
left: 20px;
|
||||||
|
top: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-panel {
|
||||||
|
width: 280px;
|
||||||
|
background: rgba(5, 5, 16, 0.8);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||||
|
border-left: 3px solid #4af0c0;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e0f0ff;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4af0c0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbolic-log-entry { margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px; }
|
||||||
|
.symbolic-rule { color: #7b5cff; display: block; }
|
||||||
|
.symbolic-outcome { color: #4af0c0; font-weight: 600; }
|
||||||
|
|
||||||
|
.blackboard-entry { font-size: 10px; margin-bottom: 2px; }
|
||||||
|
.bb-source { color: #ffd700; opacity: 0.7; }
|
||||||
|
.bb-key { color: #7b5cff; }
|
||||||
|
.bb-value { color: #fff; }
|
||||||
|
|
||||||
|
.planner-step { color: #4af0c0; margin-bottom: 2px; }
|
||||||
|
.step-num { opacity: 0.5; }
|
||||||
|
|
||||||
|
.cbr-match { color: #ffd700; font-weight: 700; margin-bottom: 2px; }
|
||||||
|
.cbr-action { color: #4af0c0; }
|
||||||
|
|
||||||
|
.neuro-bridge-entry { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
||||||
|
.neuro-icon { font-size: 14px; }
|
||||||
|
.neuro-concept { color: #7b5cff; font-weight: 600; }
|
||||||
|
|
||||||
|
.meta-stat { margin-bottom: 2px; display: flex; justify-content: space-between; }
|
||||||
|
|
||||||
|
.calibrator-entry { font-size: 10px; display: flex; gap: 8px; }
|
||||||
|
.cal-label { color: #ffd700; }
|
||||||
|
.cal-val { color: #4af0c0; }
|
||||||
|
.cal-err { color: #ff4466; opacity: 0.8; }
|
||||||
|
|
||||||
|
.nostr-pubkey { color: #ffd700; }
|
||||||
|
.nostr-status { color: #4af0c0; font-weight: 600; }
|
||||||
|
.l402-status { color: #ff4466; font-weight: 600; }
|
||||||
|
.l402-msg { color: #fff; }
|
||||||
|
|
||||||
|
.pse-status { color: #4af0c0; font-weight: 600; }
|
||||||
|
|||||||
111
tests/test_syntax_fixes.py
Normal file
111
tests/test_syntax_fixes.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""Tests for syntax and correctness fixes across the-nexus codebase.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- nexus_think.py: no stray dots (SyntaxError), no typos in argparse
|
||||||
|
- groq_worker.py: model name has no 'groq/' prefix
|
||||||
|
- server.py: uses discard() not remove() for client cleanup
|
||||||
|
- public/nexus/: corrupt duplicate directory removed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NEXUS_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# ── nexus_think.py syntax checks ────────────────────────────────────
|
||||||
|
|
||||||
|
def test_nexus_think_parses_without_syntax_error():
|
||||||
|
"""nexus_think.py must be valid Python.
|
||||||
|
|
||||||
|
Two SyntaxErrors existed:
|
||||||
|
1. Line 318: stray '.' between function call and if-block
|
||||||
|
2. Line 445: 'parser.add_.argument()' (extra underscore)
|
||||||
|
|
||||||
|
If either is present, the entire consciousness loop can't import.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
# ast.parse will raise SyntaxError if the file is invalid
|
||||||
|
try:
|
||||||
|
ast.parse(source, filename="nexus_think.py")
|
||||||
|
except SyntaxError as e:
|
||||||
|
raise AssertionError(
|
||||||
|
f"nexus_think.py has a SyntaxError at line {e.lineno}: {e.msg}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def test_nexus_think_no_stray_dot():
|
||||||
|
"""There should be no line that is just a dot in nexus_think.py."""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
for i, line in enumerate(source.splitlines(), 1):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == ".":
|
||||||
|
raise AssertionError(
|
||||||
|
f"nexus_think.py has a stray '.' on line {i}. "
|
||||||
|
"This causes a SyntaxError."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nexus_think_argparse_no_typo():
|
||||||
|
"""parser.add_argument must not be written as parser.add_.argument."""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
assert "add_.argument" not in source, (
|
||||||
|
"nexus_think.py contains 'add_.argument' — should be 'add_argument'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── groq_worker.py model name ───────────────────────────────────────
|
||||||
|
|
||||||
|
def test_groq_default_model_has_no_prefix():
|
||||||
|
"""Groq API expects model names without router prefixes.
|
||||||
|
|
||||||
|
Sending 'groq/llama3-8b-8192' returns a 404.
|
||||||
|
The correct name is just 'llama3-8b-8192'.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "groq_worker.py").read_text()
|
||||||
|
for line in source.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("DEFAULT_MODEL") and "=" in stripped:
|
||||||
|
assert "groq/" not in stripped, (
|
||||||
|
f"groq_worker.py DEFAULT_MODEL contains 'groq/' prefix: {stripped}. "
|
||||||
|
"The Groq API expects bare model names like 'llama3-8b-8192'."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# DEFAULT_MODEL not found — that's a different issue, not this test's concern
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ── server.py client cleanup ────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_server_uses_discard_not_remove():
|
||||||
|
"""server.py must use clients.discard() not clients.remove().
|
||||||
|
|
||||||
|
remove() raises KeyError if the websocket isn't in the set.
|
||||||
|
This happens if an exception occurs before clients.add() runs.
|
||||||
|
discard() is a safe no-op if the element isn't present.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "server.py").read_text()
|
||||||
|
assert "clients.discard(" in source, (
|
||||||
|
"server.py should use clients.discard(websocket) for safe cleanup."
|
||||||
|
)
|
||||||
|
assert "clients.remove(" not in source, (
|
||||||
|
"server.py should NOT use clients.remove(websocket) — "
|
||||||
|
"raises KeyError if websocket wasn't added."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── public/nexus/ corrupt duplicate directory ────────────────────────
|
||||||
|
|
||||||
|
def test_public_nexus_duplicate_removed():
|
||||||
|
"""public/nexus/ contained 3 files with identical content (all 9544 bytes).
|
||||||
|
|
||||||
|
app.js, style.css, and index.html were all the same file — clearly a
|
||||||
|
corrupt copy operation. The canonical files are at the repo root.
|
||||||
|
"""
|
||||||
|
corrupt_dir = NEXUS_ROOT / "public" / "nexus"
|
||||||
|
assert not corrupt_dir.exists(), (
|
||||||
|
"public/nexus/ still exists. These are corrupt duplicates "
|
||||||
|
"(all 3 files have identical content). Remove this directory."
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user