Stub in framework for new .net Open Jibo cloud
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -412,3 +412,7 @@ FodyWeavers.xsd
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Local .NET CLI home and first-run artifacts
|
||||
.dotnet/
|
||||
**/.dotnet/
|
||||
|
||||
|
||||
6
OpenJibo/NuGet.Config
Normal file
6
OpenJibo/NuGet.Config
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -1,2 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jibo/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jibo/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=openjibo/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1,9 +1,35 @@
|
||||
<Solution>
|
||||
<Folder Name="/docs/">
|
||||
<File Path="docs/device-bootstrap.md" />
|
||||
<File Path="docs/protocol-inventory.md" />
|
||||
<File Path="docs/public-site-plan.md" />
|
||||
<File Path="docs/support-tiers.md" />
|
||||
</Folder>
|
||||
<Folder Name="/scripts/" />
|
||||
<Folder Name="/scripts/boostrap/">
|
||||
<File Path="scripts/bootstrap/Discover-JiboHosts.ps1" />
|
||||
<File Path="scripts/bootstrap/Generate-JiboDnsOverrides.ps1" />
|
||||
<File Path="scripts/bootstrap/README.md" />
|
||||
<File Path="scripts/bootstrap/Test-OpenJiboRouting.ps1" />
|
||||
</Folder>
|
||||
<Folder Name="/scripts/cloud/">
|
||||
<File Path="scripts/cloud/Invoke-CloudSmoke.ps1" />
|
||||
<File Path="scripts/cloud/Invoke-ProtocolFixture.ps1" />
|
||||
<File Path="scripts/cloud/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Jibo.Runtime.Abstractions/Jibo.Runtime.Abstractions.csproj" Id="41abf805-373e-4e71-b1bf-2cab6dbf892e" />
|
||||
<Project Path="src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Jibo.Cloud.Domain.csproj" />
|
||||
<Project Path="src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Jibo.Cloud.Application.csproj" />
|
||||
<Project Path="src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Jibo.Cloud.Infrastructure.csproj" />
|
||||
<Project Path="src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Jibo.Cloud.Api.csproj" />
|
||||
<Project Path="src/Playground/Playground.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/OpenJibo.Site/">
|
||||
<File Path="src/OpenJibo.Site/index.html" />
|
||||
<File Path="src/OpenJibo.Site/site.css" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -1,439 +1,89 @@
|
||||
# Hybrid Jibo Runtime Plan
|
||||
# OpenJibo
|
||||
|
||||
## Goal
|
||||
## Summary
|
||||
|
||||
Build a **modern local-first Jibo runtime** while preserving the parts of native Jibo that are still useful:
|
||||
OpenJibo is the working revival track for Jibo.
|
||||
|
||||
* native wake/turn plumbing where helpful
|
||||
* native skills where helpful
|
||||
* native embodiment and rendering
|
||||
* fast experimentation in **.NET 10** off-robot
|
||||
The near-term plan is intentionally concrete:
|
||||
|
||||
Jibo’s native runtime already exposes a layered service model centered around **Jetstream** for turn/event flow, **GlobalManagerService** for routing, **SkillsService** for skill lifecycle, and **ExpressionService** for embodiment/rendering. The SSM startup is config-driven and mode-driven, which suggests a hybrid mode is a viable path.
|
||||
1. Build a stable replacement cloud on Azure.
|
||||
2. Use the existing Node prototype as the protocol oracle and capture harness.
|
||||
3. Port the hosted implementation to .NET as a modular monolith.
|
||||
4. Bring real robots online first through RCM plus controlled DNS/TLS patching.
|
||||
5. Use OTA later to reduce setup friction once the hosted cloud is proven.
|
||||
|
||||
---
|
||||
This keeps the project grounded in what is already working while moving toward a maintainable hosted platform.
|
||||
|
||||
## Architecture Direction
|
||||
## Current Truth
|
||||
|
||||
We will keep the **main experimental runtime in .NET 10** and treat Jibo as an embodied endpoint with a thin bridge layer.
|
||||
The repo now has three distinct lanes:
|
||||
|
||||
That means:
|
||||
- `src/Jibo.Cloud/node`
|
||||
The discovery server. This is the best source of observed protocol behavior today.
|
||||
- `src/Jibo.Cloud/dotnet`
|
||||
The long-term hosted implementation. This is where the stable cloud is being built.
|
||||
- `src/Jibo.Runtime.Abstractions`
|
||||
The normalized runtime seam between robot/cloud traffic and modern conversation logic.
|
||||
|
||||
* **off-robot**: conversation logic, planning, AI routing, capabilities
|
||||
* **on-robot**: thin adapter/bridge to native Jibo services
|
||||
* **native Jibo**: reuse rendering, skill hosting, and useful event seams
|
||||
|
||||
---
|
||||
|
||||
## High-Level ASCII Flowchart
|
||||
The key architectural idea is:
|
||||
|
||||
```text
|
||||
+--------------------------------------------------------------+
|
||||
| NATIVE JIBO LAYER |
|
||||
|--------------------------------------------------------------|
|
||||
| Wake / Turn Events |
|
||||
| - Jetstream |
|
||||
| - hjHeard / turn started / turn result |
|
||||
| |
|
||||
| Native Services |
|
||||
| - GlobalManagerService |
|
||||
| - SkillsService |
|
||||
| - ExpressionService |
|
||||
| - TTS / Body / Visual / Motion services |
|
||||
+------------------------------+-------------------------------+
|
||||
|
|
||||
| events / hooks / commands
|
||||
v
|
||||
+--------------------------------------------------------------+
|
||||
| JIBO BRIDGE LAYER |
|
||||
|--------------------------------------------------------------|
|
||||
| Thin adapter between Jibo and modern runtime |
|
||||
| |
|
||||
| Responsibilities: |
|
||||
| - receive turn/wake events |
|
||||
| - receive skill context / native state |
|
||||
| - forward normalized events to .NET runtime |
|
||||
| - accept ResponsePlans / commands from .NET runtime |
|
||||
| - invoke native skills / expression / TTS / visuals |
|
||||
+------------------------------+-------------------------------+
|
||||
|
|
||||
| normalized turn context
|
||||
v
|
||||
+--------------------------------------------------------------+
|
||||
| MODERN .NET 10 RUNTIME |
|
||||
|--------------------------------------------------------------|
|
||||
| Conversation Broker |
|
||||
| - session state |
|
||||
| - follow-up windows |
|
||||
| - topic/context tracking |
|
||||
| |
|
||||
| STT Strategy Selector |
|
||||
| - native transcript |
|
||||
| - local STT |
|
||||
| - cloud STT |
|
||||
| |
|
||||
| Brain Strategy Selector |
|
||||
| - skill/rules path |
|
||||
| - local AI |
|
||||
| - cloud AI |
|
||||
| - hybrid routing |
|
||||
| |
|
||||
| Action / Orchestration Planner |
|
||||
| - gestures / visuals / ESML / delegation |
|
||||
| - capability/tool calls |
|
||||
| - build final ResponsePlan |
|
||||
| |
|
||||
| Capability Registry |
|
||||
| - weather / time / reminders / tools |
|
||||
| - native skill delegation |
|
||||
| - robot expression helpers |
|
||||
+------------------------------+-------------------------------+
|
||||
|
|
||||
| ResponsePlan / commands
|
||||
v
|
||||
+--------------------------------------------------------------+
|
||||
| EXECUTION TARGETS |
|
||||
|--------------------------------------------------------------|
|
||||
| - Native SkillsService |
|
||||
| - Native ExpressionService |
|
||||
| - Native TTS / visuals / motion |
|
||||
| - Local AI backends |
|
||||
| - Cloud AI backends |
|
||||
| - External APIs / tools |
|
||||
+--------------------------------------------------------------+
|
||||
Jibo device -> OpenJibo cloud -> normalized runtime contracts -> capabilities and planning
|
||||
```
|
||||
|
||||
---
|
||||
## First Supported Device Path
|
||||
|
||||
## Runtime Flow
|
||||
The first supported recovery path is enthusiast-friendly, not zero-touch:
|
||||
|
||||
```text
|
||||
[Wake Word / Turn / Follow-up]
|
||||
|
|
||||
v
|
||||
[Jibo Native Events]
|
||||
|
|
||||
v
|
||||
[Jibo Bridge Layer]
|
||||
|
|
||||
v
|
||||
[Conversation Broker (.NET)]
|
||||
|
|
||||
v
|
||||
[STT Strategy Selection]
|
||||
|
|
||||
v
|
||||
[Brain Strategy Selection]
|
||||
/ | \
|
||||
/ | \
|
||||
[Skill/Rules] [Local AI] [Cloud AI]
|
||||
\ | /
|
||||
\ | /
|
||||
[Planner]
|
||||
|
|
||||
v
|
||||
[ResponsePlan Built]
|
||||
|
|
||||
v
|
||||
[Jibo Bridge Layer]
|
||||
|
|
||||
v
|
||||
[Skills / Expression / TTS / Motion / Visuals]
|
||||
|
|
||||
v
|
||||
[Follow-up Window or Timeout]
|
||||
QR Wi-Fi -> controlled router/DNS -> redirect legacy Jibo hosts ->
|
||||
RCM/device patch for TLS and host acceptance -> OpenJibo cloud on Azure
|
||||
```
|
||||
|
||||
---
|
||||
That path is documented in [docs/device-bootstrap.md](C:/Projects/JiboExperiments/OpenJibo/docs/device-bootstrap.md).
|
||||
|
||||
## Planned Hybrid Mode
|
||||
|
||||
Jibo’s startup and service composition are mode-driven and config-driven, so the long-term plan is to add a **new custom mode** rather than replacing stock behavior outright.
|
||||
|
||||
### Candidate mode names
|
||||
|
||||
* `hybrid`
|
||||
* `openjibo`
|
||||
* `revival`
|
||||
* `local-first`
|
||||
|
||||
### Intent of the mode
|
||||
|
||||
The custom mode should:
|
||||
|
||||
* preserve normal mode for stock behavior
|
||||
* preserve developer mode for native debugging
|
||||
* enable the bridge/runtime path for hybrid experiments
|
||||
* allow selective routing between old and new Jibo behavior
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Keep Jibo-specific code at the edges
|
||||
|
||||
The .NET runtime should know about:
|
||||
|
||||
* turns
|
||||
* sessions
|
||||
* plans
|
||||
* capabilities
|
||||
* render actions
|
||||
|
||||
It should **not** depend directly on:
|
||||
|
||||
* Electron internals
|
||||
* SSM implementation quirks
|
||||
* old Linux deployment constraints
|
||||
|
||||
### 2. Reuse native embodiment
|
||||
|
||||
Native Jibo rendering is valuable. ExpressionService appears to own animation, attention, DOF arbitration, and embodied output, so it should be reused as long as possible.
|
||||
|
||||
### 3. Replace cognition before replacing embodiment
|
||||
|
||||
The first thing to modernize is:
|
||||
|
||||
* routing
|
||||
* planning
|
||||
* AI selection
|
||||
* follow-up conversation behavior
|
||||
|
||||
Not necessarily:
|
||||
|
||||
* body motion
|
||||
* TTS
|
||||
* expression plumbing
|
||||
|
||||
### 4. Favor thin robot-side code
|
||||
|
||||
The bridge on Jibo should stay small and stable. Fast-moving logic belongs in .NET 10.
|
||||
|
||||
### 5. Everything should converge to a ResponsePlan
|
||||
|
||||
Regardless of source:
|
||||
|
||||
* skill
|
||||
* rules engine
|
||||
* local AI
|
||||
* cloud AI
|
||||
|
||||
the result should become a single normalized response/output plan.
|
||||
|
||||
---
|
||||
|
||||
## Native Jibo Mapping
|
||||
|
||||
Based on current reverse engineering, the native service boundaries map roughly like this: Jetstream is the turn/event seam, GlobalManagerService performs routing and skill-launch logic, SkillsService manages skill lifecycle, and ExpressionService handles embodiment/rendering.
|
||||
## Repo Map
|
||||
|
||||
```text
|
||||
Our Concept Native Jibo Equivalent
|
||||
---------------------------- --------------------------------
|
||||
Wake / Turn Source Jetstream
|
||||
Conversation Broker split across Jetstream + routing
|
||||
Brain Selection GlobalManagerService + skills
|
||||
Skill Execution SkillsService
|
||||
Renderer / Embodiment ExpressionService
|
||||
OpenJibo/
|
||||
docs/
|
||||
device-bootstrap.md
|
||||
protocol-inventory.md
|
||||
public-site-plan.md
|
||||
support-tiers.md
|
||||
|
||||
scripts/bootstrap/
|
||||
Discover-JiboHosts.ps1
|
||||
Generate-JiboDnsOverrides.ps1
|
||||
Test-OpenJiboRouting.ps1
|
||||
|
||||
src/
|
||||
Jibo.Cloud/
|
||||
node/
|
||||
dotnet/
|
||||
Jibo.Runtime.Abstractions/
|
||||
Playground/
|
||||
OpenJibo.Site/
|
||||
```
|
||||
|
||||
---
|
||||
## Decisions Locked In
|
||||
|
||||
## Proposed Project Layout
|
||||
- The first milestone is `core revive`, not full protocol parity.
|
||||
- Azure SQL is the relational system of record for the hosted cloud.
|
||||
- Billing and donations are future-compatible concerns, not phase-one delivery requirements.
|
||||
- OTA is a phase-two simplification strategy, not the initial dependency.
|
||||
|
||||
```text
|
||||
/src
|
||||
/Jibo.Runtime
|
||||
Core runtime orchestration
|
||||
- ConversationBroker
|
||||
- Session state
|
||||
- Turn pipeline
|
||||
- ResponsePlan builder
|
||||
## Near-Term Work
|
||||
|
||||
/Jibo.Runtime.Abstractions
|
||||
Interfaces and models
|
||||
- ITurnSource
|
||||
- ISttStrategy
|
||||
- IBrainStrategy
|
||||
- IResponsePlanner
|
||||
- IRobotAdapter
|
||||
- TurnContext
|
||||
- ResponsePlan
|
||||
- port required endpoint and WebSocket behavior from Node to .NET
|
||||
- keep protocol captures and replay fixtures current
|
||||
- harden device bootstrap documentation and scripts
|
||||
- stand up the initial `openjibo.com` information site
|
||||
|
||||
/Jibo.Bridge
|
||||
Jibo adapter / compatibility layer
|
||||
- robot event ingestion
|
||||
- command dispatch back to Jibo
|
||||
- native hook integration
|
||||
## Important Docs
|
||||
|
||||
/Jibo.Brain.Rules
|
||||
deterministic routing / skills / decision tree
|
||||
|
||||
/Jibo.Brain.Local
|
||||
local AI experiments
|
||||
|
||||
/Jibo.Brain.Cloud
|
||||
cloud AI experiments
|
||||
|
||||
/Jibo.Capabilities
|
||||
tools and callable capabilities
|
||||
- weather
|
||||
- time
|
||||
- reminders
|
||||
- skill delegation
|
||||
- expression helpers
|
||||
|
||||
/Jibo.Simulator
|
||||
fake robot target for testing ResponsePlans
|
||||
|
||||
/docs
|
||||
architecture
|
||||
notes
|
||||
traces
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Initial Build Plan
|
||||
|
||||
### Phase 1 — Contracts and runtime skeleton
|
||||
|
||||
Build the core models and interfaces first:
|
||||
|
||||
* `TurnContext`
|
||||
* `ConversationSession`
|
||||
* `SttResult`
|
||||
* `BrainDecision`
|
||||
* `ResponsePlan`
|
||||
* `RenderAction`
|
||||
* `FollowupPolicy`
|
||||
|
||||
### Phase 2 — Minimal broker
|
||||
|
||||
Implement:
|
||||
|
||||
* session open/close
|
||||
* follow-up timeout
|
||||
* topic/context tracking
|
||||
|
||||
### Phase 3 — Bridge skeleton
|
||||
|
||||
Create the adapter boundary for:
|
||||
|
||||
* inbound Jibo events
|
||||
* outbound robot commands
|
||||
|
||||
Even if the first version is mocked, keep the interface stable.
|
||||
|
||||
### Phase 4 — First working path
|
||||
|
||||
Implement a narrow vertical slice:
|
||||
|
||||
* input turn
|
||||
* decision/rules path
|
||||
* weather example
|
||||
* TTS response
|
||||
* follow-up window
|
||||
|
||||
### Phase 5 — Native integration expansion
|
||||
|
||||
Add native delegation for:
|
||||
|
||||
* skills
|
||||
* expression
|
||||
* visuals
|
||||
* gestures
|
||||
* local turn/open follow-up behavior
|
||||
|
||||
### Phase 6 — Hybrid AI routing
|
||||
|
||||
Add:
|
||||
|
||||
* local AI path
|
||||
* cloud AI path
|
||||
* confidence/routing policy
|
||||
|
||||
---
|
||||
|
||||
## First Vertical Slice
|
||||
|
||||
Recommended first demonstration:
|
||||
|
||||
### Example
|
||||
|
||||
User says:
|
||||
|
||||
> Hey Jibo, what’s the weather?
|
||||
|
||||
System flow:
|
||||
|
||||
1. Jibo event arrives through bridge
|
||||
2. .NET broker opens a session
|
||||
3. transcript enters routing
|
||||
4. weather capability is called
|
||||
5. planner builds a `ResponsePlan`
|
||||
6. bridge sends speech + visual action back to Jibo
|
||||
7. follow-up window stays open
|
||||
|
||||
Then:
|
||||
|
||||
> What about the low tonight?
|
||||
|
||||
The same session stays active without wake word if the follow-up window is still open.
|
||||
|
||||
---
|
||||
|
||||
## Near-Term Questions to Answer
|
||||
|
||||
* What is the cleanest robot-side bridge seam:
|
||||
|
||||
* Jetstream hook
|
||||
* skill hook
|
||||
* local service calls
|
||||
* mixed approach
|
||||
|
||||
* What is the smallest command set needed to drive Jibo usefully:
|
||||
|
||||
* speak
|
||||
* gesture
|
||||
* visual
|
||||
* launch skill
|
||||
* keep listening
|
||||
|
||||
* Which pieces should remain native the longest:
|
||||
|
||||
* expression
|
||||
* skill hosting
|
||||
* turn engine
|
||||
* wake-word flow
|
||||
|
||||
* How should custom mode selection activate the hybrid path
|
||||
|
||||
---
|
||||
|
||||
## Practical Strategy
|
||||
|
||||
For now:
|
||||
|
||||
* **develop fast in .NET 10**
|
||||
* **use Jibo as an embodied endpoint**
|
||||
* **keep the robot-side integration thin**
|
||||
* **delay deep on-robot porting until architecture proves itself**
|
||||
|
||||
This keeps experimentation fast while preserving a path toward deeper integration later.
|
||||
|
||||
---
|
||||
|
||||
## Current Working Hypothesis
|
||||
|
||||
The best long-term shape is:
|
||||
|
||||
```text
|
||||
stock Jibo embodiment + modern external cognition + thin hybrid bridge
|
||||
```
|
||||
|
||||
That gives us:
|
||||
|
||||
* rapid iteration
|
||||
* local-first experiments
|
||||
* preserved native robot personality/expression
|
||||
* reduced dependence on brittle legacy cloud paths
|
||||
- [Cloud overview](/src/Jibo.Cloud/README.md)
|
||||
- [Protocol inventory](/docs/protocol-inventory.md)
|
||||
- [Support tiers](/docs/support-tiers.md)
|
||||
- [Device bootstrap path](/docs/device-bootstrap.md)
|
||||
- [Public site plan](/docs/public-site-plan.md)
|
||||
66
OpenJibo/docs/device-bootstrap.md
Normal file
66
OpenJibo/docs/device-bootstrap.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Device Bootstrap Path
|
||||
|
||||
## Supported First Path
|
||||
|
||||
The first supported OpenJibo recovery path is:
|
||||
|
||||
```text
|
||||
QR Wi-Fi -> controlled router/DNS -> redirect Jibo hosts ->
|
||||
RCM/device patch -> Azure-hosted OpenJibo cloud
|
||||
```
|
||||
|
||||
This is the path we can document, repeat, and improve.
|
||||
|
||||
## Why This Path Comes First
|
||||
|
||||
- it matches what the current Node prototype already requires
|
||||
- it keeps the hosted cloud work grounded in real device traffic
|
||||
- it avoids blocking the entire revival on OTA before cloud compatibility exists
|
||||
|
||||
## Bootstrap Checklist
|
||||
|
||||
1. Connect the robot to a controlled Wi-Fi network.
|
||||
2. Redirect legacy cloud hostnames to the OpenJibo environment.
|
||||
3. Prevent fallback DNS from bypassing the controlled resolver.
|
||||
4. Gain RCM/device access for targeted TLS or host validation changes.
|
||||
5. Verify robot startup, token flow, socket flow, and first-turn behavior.
|
||||
|
||||
## Required Host Routing
|
||||
|
||||
At minimum, watch and validate:
|
||||
|
||||
- `api.jibo.com`
|
||||
- `api-socket.jibo.com`
|
||||
- `neo-hub.jibo.com`
|
||||
- `neohub.jibo.com`
|
||||
|
||||
## Scripted Helpers
|
||||
|
||||
Bootstrap helper scripts live in [scripts/bootstrap](C:/Projects/JiboExperiments/OpenJibo/scripts/bootstrap):
|
||||
|
||||
- `Discover-JiboHosts.ps1`
|
||||
- `Generate-JiboDnsOverrides.ps1`
|
||||
- `Test-OpenJiboRouting.ps1`
|
||||
|
||||
These are intentionally conservative helpers for discovery and verification, not destructive patch tools.
|
||||
|
||||
## TLS And Runtime Patching
|
||||
|
||||
Patching requirements will vary by device version and by where certificate validation is enforced.
|
||||
|
||||
Near-term guidance:
|
||||
|
||||
- record each patch location by software version
|
||||
- prefer small, repeatable changes over ad hoc edits
|
||||
- keep a versioned host inventory and patch checklist
|
||||
- do not describe OTA as the primary bootstrap method until the hosted cloud is stable
|
||||
|
||||
## Smoke Test Goals
|
||||
|
||||
The first real-device smoke test should confirm:
|
||||
|
||||
- robot startup reaches the hosted cloud
|
||||
- token issuance succeeds
|
||||
- required sockets connect
|
||||
- the robot can complete one simple turn
|
||||
- update metadata calls do not break startup
|
||||
70
OpenJibo/docs/protocol-inventory.md
Normal file
70
OpenJibo/docs/protocol-inventory.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Protocol Inventory
|
||||
|
||||
## Purpose
|
||||
|
||||
This document tracks the currently observed cloud surface area for Jibo and helps keep the .NET port aligned with real behavior captured by the Node prototype.
|
||||
|
||||
Confidence levels:
|
||||
|
||||
- `high`: observed in code and currently represented in the .NET scaffold
|
||||
- `medium`: observed in the Node oracle and documented, but not fully ported yet
|
||||
- `low`: expected or inferred, needs more robot validation
|
||||
|
||||
## Known Hosts
|
||||
|
||||
| Host | Purpose | Confidence | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `api.jibo.com` | HTTPS API target for `X-Amz-Target` operations | high | Main request dispatch path in the Node prototype |
|
||||
| `api-socket.jibo.com` | token-authenticated WebSocket path | medium | Node accepts tokenized connections and intentionally sends no greeting |
|
||||
| `neo-hub.jibo.com` | listen and proactive WebSocket traffic | medium | Path-driven split between listen and `/v1/proactive` |
|
||||
| `neohub.jibo.com` | likely alias/spelling variant to watch | low | Mentioned in docs; validate against real traffic |
|
||||
|
||||
## HTTP Dispatch Families
|
||||
|
||||
Observed from `open-jibo-link.js`:
|
||||
|
||||
| Service family | Example operations | Confidence | Current .NET status |
|
||||
| --- | --- | --- | --- |
|
||||
| `Account_*` | `CreateHubToken`, `CreateAccessToken`, `Login`, `Get` | high | initial dispatch implemented |
|
||||
| `Notification_*` | `NewRobotToken` | high | initial dispatch implemented |
|
||||
| `Loop_*` | `List`, `ListLoops` | medium | initial dispatch implemented |
|
||||
| `Robot_*` | `GetRobot`, `UpdateRobot` | medium | initial dispatch implemented |
|
||||
| `Update_*` | `ListUpdates`, `ListUpdatesFrom`, `GetUpdateFrom`, `CreateUpdate`, `RemoveUpdate` | medium | list/get scaffolding implemented |
|
||||
| `Media_20160725` | `List`, `Get`, `Create`, `Remove` | medium | not yet ported |
|
||||
| `Log_*` | `PutEvents`, `PutEventsAsync`, `PutBinaryAsync`, `PutAsrBinary` | medium | upload endpoints reserved; detailed handling pending |
|
||||
| `Key_*` | `ShouldCreate`, `CreateSymmetricKey`, `GetRequest` | medium | pending |
|
||||
| `Person_*` | `ListHolidays` | low | pending |
|
||||
| `Backup_*` | `List` | low | pending |
|
||||
|
||||
## WebSocket Flows
|
||||
|
||||
| Host/path | Flow | Confidence | Current .NET status |
|
||||
| --- | --- | --- | --- |
|
||||
| `api-socket.jibo.com/{token}` | token-authenticated socket for API-side signaling | medium | stub endpoint implemented |
|
||||
| `neo-hub.jibo.com/{listen-path}` | listen turn flow with JSON and binary audio traffic | medium | initial JSON flow implemented |
|
||||
| `neo-hub.jibo.com/v1/proactive` | proactive connection flow | medium | stub endpoint implemented |
|
||||
|
||||
## Upload Paths
|
||||
|
||||
| Path | Purpose | Confidence | Current .NET status |
|
||||
| --- | --- | --- | --- |
|
||||
| `/upload/asr-binary` | async audio/log upload target | medium | placeholder endpoint accepted |
|
||||
| `/upload/log-events` | async log upload target | medium | placeholder endpoint accepted |
|
||||
| `/upload/log-binary` | async binary upload target | medium | placeholder endpoint accepted |
|
||||
|
||||
## First Core Revive Slice
|
||||
|
||||
The first .NET hosted milestone should fully support:
|
||||
|
||||
- `Account.CreateHubToken`
|
||||
- `Notification.NewRobotToken`
|
||||
- `Loop.List` and `Loop.ListLoops`
|
||||
- `Robot.GetRobot`
|
||||
- `Update.ListUpdates`, `Update.ListUpdatesFrom`, `Update.GetUpdateFrom`
|
||||
- root probe and health checks
|
||||
- basic listen/proactive WebSocket acceptance
|
||||
- normalized turn and reply mapping for simple chat
|
||||
|
||||
## Fixture Source
|
||||
|
||||
Sanitized fixtures live under [src/Jibo.Cloud/node/fixtures](C:/Projects/JiboExperiments/OpenJibo/src/Jibo.Cloud/node/fixtures) and should be expanded as real traffic is captured.
|
||||
24
OpenJibo/docs/public-site-plan.md
Normal file
24
OpenJibo/docs/public-site-plan.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Public Site Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Stand up a small public site on `openjibo.com` that makes the project understandable in a few minutes.
|
||||
|
||||
## First Version Content
|
||||
|
||||
- project overview
|
||||
- current status
|
||||
- links to source repositories
|
||||
- links to device bootstrap docs
|
||||
- explanation of the hosted-cloud direction
|
||||
- contribution/contact or waitlist path
|
||||
|
||||
## What The Site Should Not Pretend Yet
|
||||
|
||||
- zero-touch recovery
|
||||
- complete parity with the original cloud
|
||||
- public production readiness before device validation is repeatable
|
||||
|
||||
## Initial Repo Asset
|
||||
|
||||
A simple static site scaffold lives in [src/OpenJibo.Site](C:/Projects/JiboExperiments/OpenJibo/src/OpenJibo.Site).
|
||||
33
OpenJibo/docs/support-tiers.md
Normal file
33
OpenJibo/docs/support-tiers.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Support Tiers
|
||||
|
||||
## Purpose
|
||||
|
||||
This document keeps the revival effort honest about what must work first, what can wait for parity, and what belongs to later modernization.
|
||||
|
||||
## Required For Core Revive
|
||||
|
||||
- hosted replacement cloud reachable through legacy Jibo host routing
|
||||
- token, account, robot, and session bootstrap flows needed for startup
|
||||
- basic WebSocket connectivity for listen and proactive channels
|
||||
- minimal turn handling and a normalized `ResponsePlan` path
|
||||
- Azure deployment foundation
|
||||
- Azure SQL-backed persistence design
|
||||
- bootstrap documentation for router, DNS, RCM, TLS patching, and smoke tests
|
||||
|
||||
## Optional For Parity
|
||||
|
||||
- broader `X-Amz-Target` family coverage
|
||||
- richer media management
|
||||
- more complete key and sharing flows
|
||||
- higher-fidelity update metadata behavior
|
||||
- more native-skill bridging and expression parity
|
||||
- more complete per-version device behavior mapping
|
||||
|
||||
## Future Modernization
|
||||
|
||||
- OTA-first recovery for non-technical owners
|
||||
- paid hosted plans, subscriptions, and donation flows
|
||||
- deeper on-device modernization
|
||||
- richer runtime orchestration and AI providers
|
||||
- community plugin or skill ecosystem
|
||||
- OS, bridge, and firmware modernization beyond hosted-cloud recovery
|
||||
34
OpenJibo/scripts/bootstrap/Discover-JiboHosts.ps1
Normal file
34
OpenJibo/scripts/bootstrap/Discover-JiboHosts.ps1
Normal file
@@ -0,0 +1,34 @@
|
||||
param(
|
||||
[string]$LogDirectory = ".",
|
||||
[string[]]$KnownHosts = @(
|
||||
"api.jibo.com",
|
||||
"api-socket.jibo.com",
|
||||
"neo-hub.jibo.com",
|
||||
"neohub.jibo.com"
|
||||
)
|
||||
)
|
||||
|
||||
$resolved = foreach ($host in $KnownHosts) {
|
||||
try {
|
||||
$dns = Resolve-DnsName -Name $host -ErrorAction Stop
|
||||
[pscustomobject]@{
|
||||
Host = $host
|
||||
Addresses = ($dns | Where-Object { $_.IPAddress } | Select-Object -ExpandProperty IPAddress) -join ", "
|
||||
ObservedUtc = [DateTime]::UtcNow.ToString("o")
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
Host = $host
|
||||
Addresses = "<unresolved>"
|
||||
ObservedUtc = [DateTime]::UtcNow.ToString("o")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$resolved | Tee-Object -Variable rows | Format-Table -AutoSize
|
||||
|
||||
$outputPath = Join-Path $LogDirectory "jibo-host-discovery.json"
|
||||
$rows | ConvertTo-Json -Depth 4 | Set-Content -Path $outputPath
|
||||
|
||||
Write-Host "Saved discovery report to $outputPath"
|
||||
23
OpenJibo/scripts/bootstrap/Generate-JiboDnsOverrides.ps1
Normal file
23
OpenJibo/scripts/bootstrap/Generate-JiboDnsOverrides.ps1
Normal file
@@ -0,0 +1,23 @@
|
||||
param(
|
||||
[string]$TargetIp,
|
||||
[string[]]$HostNames = @(
|
||||
"api.jibo.com",
|
||||
"api-socket.jibo.com",
|
||||
"neo-hub.jibo.com",
|
||||
"neohub.jibo.com"
|
||||
)
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TargetIp)) {
|
||||
throw "TargetIp is required."
|
||||
}
|
||||
|
||||
$entries = foreach ($host in $HostNames) {
|
||||
[pscustomobject]@{
|
||||
Host = $host
|
||||
TargetIp = $TargetIp
|
||||
HostsFileLine = "$TargetIp`t$host"
|
||||
}
|
||||
}
|
||||
|
||||
$entries | Format-Table -AutoSize
|
||||
9
OpenJibo/scripts/bootstrap/README.md
Normal file
9
OpenJibo/scripts/bootstrap/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Bootstrap Scripts
|
||||
|
||||
These scripts support the first OpenJibo recovery path:
|
||||
|
||||
- discover which hosts the robot is trying to reach
|
||||
- generate DNS override records for a controlled environment
|
||||
- verify that the robot-facing domains resolve and answer as expected
|
||||
|
||||
They are intentionally non-destructive.
|
||||
30
OpenJibo/scripts/bootstrap/Test-OpenJiboRouting.ps1
Normal file
30
OpenJibo/scripts/bootstrap/Test-OpenJiboRouting.ps1
Normal file
@@ -0,0 +1,30 @@
|
||||
param(
|
||||
[string[]]$Hosts = @(
|
||||
"https://api.jibo.com/health",
|
||||
"https://api-socket.jibo.com/",
|
||||
"https://neo-hub.jibo.com/v1/proactive"
|
||||
)
|
||||
)
|
||||
|
||||
foreach ($url in $Hosts) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $url -Method Get -SkipCertificateCheck -ErrorAction Stop
|
||||
[pscustomobject]@{
|
||||
Url = $url
|
||||
StatusCode = $response.StatusCode
|
||||
Success = $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
Url = $url
|
||||
StatusCode = $statusCode
|
||||
Success = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
32
OpenJibo/scripts/cloud/Invoke-CloudSmoke.ps1
Normal file
32
OpenJibo/scripts/cloud/Invoke-CloudSmoke.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:5000"
|
||||
)
|
||||
|
||||
$checks = @(
|
||||
@{ Name = "Health"; Method = "GET"; Url = "$BaseUrl/health"; Headers = @{}; Body = $null },
|
||||
@{ Name = "CreateHubToken"; Method = "POST"; Url = "$BaseUrl/"; Headers = @{ "X-Amz-Target" = "Account_20160715.CreateHubToken"; Host = "api.jibo.com" }; Body = "{}" },
|
||||
@{ Name = "NewRobotToken"; Method = "POST"; Url = "$BaseUrl/"; Headers = @{ "X-Amz-Target" = "Notification_20160715.NewRobotToken"; Host = "api.jibo.com" }; Body = '{"deviceId":"my-robot-serial-number"}' }
|
||||
)
|
||||
|
||||
foreach ($check in $checks) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $check.Url -Method $check.Method -Headers $check.Headers -Body $check.Body -ContentType "application/json"
|
||||
[pscustomobject]@{
|
||||
Name = $check.Name
|
||||
StatusCode = $response.StatusCode
|
||||
Success = $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $null
|
||||
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
Name = $check.Name
|
||||
StatusCode = $statusCode
|
||||
Success = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
37
OpenJibo/scripts/cloud/Invoke-ProtocolFixture.ps1
Normal file
37
OpenJibo/scripts/cloud/Invoke-ProtocolFixture.ps1
Normal file
@@ -0,0 +1,37 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:5000",
|
||||
[string]$FixturePath
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($FixturePath)) {
|
||||
throw "FixturePath is required."
|
||||
}
|
||||
|
||||
$fixture = Get-Content $FixturePath | ConvertFrom-Json
|
||||
$headers = @{}
|
||||
|
||||
foreach ($property in $fixture.headers.PSObject.Properties) {
|
||||
$headers[$property.Name] = [string]$property.Value
|
||||
}
|
||||
|
||||
if (-not $headers.ContainsKey("Host") -and $fixture.host) {
|
||||
$headers["Host"] = [string]$fixture.host
|
||||
}
|
||||
|
||||
$body = ""
|
||||
if ($null -ne $fixture.body) {
|
||||
$body = $fixture.body | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
$response = Invoke-WebRequest `
|
||||
-Uri ($BaseUrl + [string]$fixture.path) `
|
||||
-Method ([string]$fixture.method) `
|
||||
-Headers $headers `
|
||||
-Body $body `
|
||||
-ContentType "application/json"
|
||||
|
||||
[pscustomobject]@{
|
||||
Fixture = $FixturePath
|
||||
StatusCode = $response.StatusCode
|
||||
Body = $response.Content
|
||||
}
|
||||
8
OpenJibo/scripts/cloud/README.md
Normal file
8
OpenJibo/scripts/cloud/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Cloud Scripts
|
||||
|
||||
These scripts help exercise the new .NET hosted cloud locally.
|
||||
|
||||
- `Invoke-CloudSmoke.ps1`
|
||||
Runs a few quick HTTP checks against a local OpenJibo cloud instance.
|
||||
- `Invoke-ProtocolFixture.ps1`
|
||||
Replays a sanitized HTTP fixture against a running local instance.
|
||||
@@ -1,97 +1,62 @@
|
||||
# Jibo.Cloud
|
||||
|
||||
## Overview
|
||||
## Summary
|
||||
|
||||
**Jibo.Cloud** is the replacement cloud layer for the OpenJibo project.
|
||||
`Jibo.Cloud` is the replacement cloud layer for OpenJibo.
|
||||
|
||||
The original Jibo relied heavily on cloud services for core functionality including speech, skills, configuration, and identity. With the original cloud infrastructure no longer available, this project aims to recreate and eventually improve that layer so Jibo can function again for everyday users.
|
||||
Its job is to restore the hosted services that physical Jibo devices still expect, while also becoming the bridge into a modern .NET runtime and future capabilities.
|
||||
|
||||
This is not just a mock or emulator. The goal is to build a functional, extensible cloud platform that can support both the original Jibo behaviors and new capabilities over time.
|
||||
## Current Strategy
|
||||
|
||||
---
|
||||
The project is deliberately split into two roles:
|
||||
|
||||
## Current Approach
|
||||
- `node/`
|
||||
Reverse-engineering oracle, discovery server, fixture source, and rapid protocol lab.
|
||||
- `dotnet/`
|
||||
Stable hosted implementation intended for Azure deployment and long-term maintenance.
|
||||
|
||||
The cloud layer is being developed in stages.
|
||||
The Node server remains valuable, but it is no longer the target production architecture.
|
||||
|
||||
To move quickly and understand Jibo’s behavior, development started with a lightweight Node.js implementation that acts as a “fake cloud.” This allows rapid experimentation, endpoint discovery, and validation of how Jibo communicates.
|
||||
## First Production Goal
|
||||
|
||||
As the system stabilizes, the implementation is being ported to **C# / .NET** for long-term maintainability, performance, and integration with hosted infrastructure.
|
||||
The first milestone is a stable hosted cloud that can support:
|
||||
|
||||
---
|
||||
- token and session issuance
|
||||
- account and robot identity flows needed for startup
|
||||
- required HTTPS `X-Amz-Target` operations
|
||||
- required WebSocket listen and proactive flows
|
||||
- basic media and update metadata handling
|
||||
- normalized handoff into OpenJibo runtime contracts
|
||||
|
||||
## Architecture Direction
|
||||
## Hosting Direction
|
||||
|
||||
The long-term vision for Jibo.Cloud is:
|
||||
The hosted deployment target is Azure:
|
||||
|
||||
* Provide a stable replacement for Jibo’s original cloud endpoints
|
||||
* Support secure communication (TLS) using a real hosted domain
|
||||
* Act as a bridge between the physical robot and the OpenJibo runtime
|
||||
* Enable new capabilities beyond the original Jibo feature set
|
||||
- Azure App Service with WebSockets enabled
|
||||
- Azure SQL as the system of record
|
||||
- Azure Blob Storage for upload and update artifacts
|
||||
- Azure Key Vault for secrets and certificates
|
||||
- Application Insights for telemetry and diagnostics
|
||||
|
||||
### OTA Update Strategy
|
||||
Human-facing entry points will live on domains such as:
|
||||
|
||||
One of the key strategies for restoring and extending Jibo functionality is leveraging its existing **OTA (over-the-air) update mechanism**.
|
||||
- `openjibo.com`
|
||||
- `openjibo.ai`
|
||||
|
||||
Rather than requiring users to manually modify their devices, Jibo.Cloud aims to:
|
||||
Robot traffic may still arrive using legacy hostnames routed to the OpenJibo service.
|
||||
|
||||
* Deliver updates through Jibo’s native update flow
|
||||
* Push new or modified skills directly to the robot
|
||||
* Eventually enable delivery of larger system updates (including OpenJibo components)
|
||||
## Recovery Strategy
|
||||
|
||||
This approach significantly lowers the barrier for non-technical users and creates a path toward a true “plug-and-play” recovery experience.
|
||||
The first supported device path is:
|
||||
|
||||
---
|
||||
|
||||
## Hosting Strategy
|
||||
|
||||
The cloud service is intended to be hosted publicly using domains such as:
|
||||
|
||||
* `openjibo.com`
|
||||
* `openjibo.ai`
|
||||
|
||||
Final domain structure is still being evaluated and may include subdomains similar to the original Jibo architecture.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```plaintext id="6h2v1k"
|
||||
Jibo.Cloud/
|
||||
node/ # Initial prototype implementation (Node.js)
|
||||
dotnet/ # Long-term implementation (C# / .NET)
|
||||
```text
|
||||
RCM + controlled DNS/TLS patching + hosted OpenJibo cloud
|
||||
```
|
||||
|
||||
---
|
||||
OTA remains important, but it is a later simplification layer after the hosted cloud is stable on real hardware.
|
||||
|
||||
## Goals
|
||||
|
||||
* Restore functionality to existing Jibo devices
|
||||
* Provide an “easy button” for non-technical users
|
||||
* Leverage OTA updates to simplify delivery and adoption
|
||||
* Keep the system open and extensible for the community
|
||||
* Build a foundation for future OpenJibo capabilities
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
This project is actively in development.
|
||||
|
||||
* Node.js prototype: in progress and functional for basic interactions
|
||||
* C# implementation: planned and in progress
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in helping, exploring, or building on this work, contributions are welcome.
|
||||
|
||||
The goal is to make Jibo accessible again, not just for developers, but for anyone who owns one.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
This project is not affiliated with the original Jibo company. It is a community-driven effort to restore and extend the platform.
|
||||
## Supporting Docs
|
||||
|
||||
- [Protocol inventory](C:/Projects/JiboExperiments/OpenJibo/docs/protocol-inventory.md)
|
||||
- [Support tiers](C:/Projects/JiboExperiments/OpenJibo/docs/support-tiers.md)
|
||||
- [Device bootstrap path](C:/Projects/JiboExperiments/OpenJibo/docs/device-bootstrap.md)
|
||||
|
||||
@@ -1,204 +1,65 @@
|
||||
# Jibo.Cloud.DotNet
|
||||
|
||||
## Overview
|
||||
## Summary
|
||||
|
||||
**Jibo.Cloud.DotNet** is the long-term, production-focused implementation of the OpenJibo cloud layer.
|
||||
`Jibo.Cloud.DotNet` is the stable hosted implementation of the OpenJibo cloud.
|
||||
|
||||
While the Node.js implementation was used to rapidly explore and validate Jibo’s cloud behavior, this project represents the future direction: a clean, maintainable, and scalable cloud platform built on **C# and .NET**.
|
||||
This is the production-oriented path for restoring device connectivity and creating a foundation for future runtime, AI, and OTA work.
|
||||
|
||||
This is where the OpenJibo cloud becomes real.
|
||||
## Architecture
|
||||
|
||||
---
|
||||
The first implementation is a modular monolith:
|
||||
|
||||
## Vision
|
||||
|
||||
The goal of this project is not just to replicate the original Jibo cloud, but to build something better:
|
||||
|
||||
* A stable and secure cloud platform for Jibo devices
|
||||
* A bridge between the physical robot and modern AI-driven systems
|
||||
* A foundation for new capabilities beyond what Jibo originally supported
|
||||
|
||||
This is the backbone of the OpenJibo ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Clean Architecture
|
||||
|
||||
This implementation is designed around clear separation of concerns:
|
||||
|
||||
* Transport (HTTP, WebSocket)
|
||||
* Application logic (routing, orchestration)
|
||||
* Domain models (robot, session, capabilities)
|
||||
* Integration layers (AI, storage, external services)
|
||||
|
||||
---
|
||||
|
||||
### 2. Compatibility First
|
||||
|
||||
The system will:
|
||||
|
||||
* Emulate required Jibo cloud endpoints
|
||||
* Support existing device expectations
|
||||
* Preserve OTA update compatibility
|
||||
|
||||
This ensures existing devices can reconnect without invasive changes.
|
||||
|
||||
---
|
||||
|
||||
### 3. Extensibility
|
||||
|
||||
The platform is being designed to support:
|
||||
|
||||
* New skills and capabilities
|
||||
* AI-driven conversation and planning
|
||||
* External integrations (APIs, services, tools)
|
||||
* Multi-agent orchestration (future CoffeeBreak integration)
|
||||
|
||||
---
|
||||
|
||||
### 4. Cloud-Native Deployment
|
||||
|
||||
The target deployment model includes:
|
||||
|
||||
* Azure-hosted services
|
||||
* Real domains (`openjibo.com`, `openjibo.ai`)
|
||||
* Proper TLS / certificate chains
|
||||
* Scalable service architecture
|
||||
|
||||
---
|
||||
|
||||
## Role in the OpenJibo Architecture
|
||||
|
||||
Jibo.Cloud sits between the robot and the runtime:
|
||||
|
||||
```plaintext id="l3tq9n"
|
||||
Jibo Device
|
||||
↓
|
||||
Jibo.Cloud (this project)
|
||||
↓
|
||||
OpenJibo Runtime (.NET)
|
||||
↓
|
||||
Capabilities / AI / Services
|
||||
```text
|
||||
Api -> Application -> Domain -> Infrastructure
|
||||
```
|
||||
|
||||
Responsibilities include:
|
||||
This keeps deployment simple while preserving clean boundaries.
|
||||
|
||||
* Handling device communication (HTTPS + WebSockets)
|
||||
* Managing identity, sessions, and tokens
|
||||
* Routing requests to runtime services
|
||||
* Delivering OTA updates
|
||||
* Acting as the central coordination layer
|
||||
## Azure Direction
|
||||
|
||||
---
|
||||
The target Azure footprint is:
|
||||
|
||||
## OTA Update Strategy
|
||||
- Azure App Service for HTTP and WebSocket traffic
|
||||
- Azure SQL for relational persistence
|
||||
- Azure Blob Storage for uploads and update artifacts
|
||||
- Azure Key Vault for secrets and certificates
|
||||
- Application Insights for observability
|
||||
|
||||
A key part of this implementation is full support for Jibo’s OTA update mechanism.
|
||||
Azure SQL is the primary system of record for:
|
||||
|
||||
This enables:
|
||||
- accounts
|
||||
- devices
|
||||
- sessions
|
||||
- update metadata
|
||||
- host mappings
|
||||
- bootstrap and provisioning records
|
||||
|
||||
* Delivery of updated skills
|
||||
* Deployment of new capabilities
|
||||
* Gradual rollout of OpenJibo runtime components
|
||||
* A path toward restoring devices without manual intervention
|
||||
## Compatibility Goal
|
||||
|
||||
The goal is to make recovery and updates feel native to the device.
|
||||
The first compatibility milestone is `core revive`.
|
||||
|
||||
---
|
||||
That means the .NET cloud should handle:
|
||||
|
||||
## Planned Features
|
||||
- token and session issuance
|
||||
- account and robot identity flows needed for startup
|
||||
- core `X-Amz-Target` dispatch
|
||||
- listen and proactive WebSocket paths
|
||||
- basic media and update metadata responses
|
||||
- handoff into normalized `TurnContext` and `ResponsePlan` contracts
|
||||
|
||||
This project will evolve to support:
|
||||
## Relationship To The Node Prototype
|
||||
|
||||
### Core Platform
|
||||
The Node server remains the discovery harness and fixture source.
|
||||
|
||||
* HTTPS API endpoints compatible with Jibo
|
||||
* WebSocket communication layer
|
||||
* Authentication and token services
|
||||
* Device and user management
|
||||
The .NET implementation should:
|
||||
|
||||
### Runtime Integration
|
||||
- copy observed behavior where needed
|
||||
- use fixtures captured from Node and real robots
|
||||
- avoid speculative protocol design
|
||||
|
||||
* Conversation orchestration
|
||||
* Capability routing
|
||||
* Response planning
|
||||
* Integration with OpenJibo runtime abstractions
|
||||
## Current State
|
||||
|
||||
### AI Integration
|
||||
|
||||
* Speech-to-text and text-to-speech
|
||||
* Intent recognition and planning
|
||||
* External AI providers (pluggable)
|
||||
|
||||
### Update System
|
||||
|
||||
* OTA update orchestration
|
||||
* Skill delivery pipeline
|
||||
* Versioning and rollout control
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
This folder will contain one or more .NET projects, likely including:
|
||||
|
||||
```plaintext id="2qk0a7"
|
||||
dotnet/
|
||||
Jibo.Cloud.Api/ # HTTP + WebSocket endpoints
|
||||
Jibo.Cloud.Application/ # Application logic and orchestration
|
||||
Jibo.Cloud.Domain/ # Core models and contracts
|
||||
Jibo.Cloud.Infrastructure/ # External integrations (storage, AI, etc.)
|
||||
```
|
||||
|
||||
Structure may evolve as the system matures.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to Node Implementation
|
||||
|
||||
The Node.js implementation remains valuable as:
|
||||
|
||||
* A reference for endpoint behavior
|
||||
* A rapid testing environment
|
||||
* A discovery tool for protocol details
|
||||
|
||||
However, this .NET implementation is the intended long-term solution.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
This project is in early development.
|
||||
|
||||
* Core abstractions are being defined
|
||||
* Endpoint behavior is being mapped from the Node implementation
|
||||
* Initial service scaffolding is planned
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in building the future of Jibo, this is the place to do it.
|
||||
|
||||
Areas where help is especially valuable:
|
||||
|
||||
* API design and endpoint mapping
|
||||
* WebSocket protocol handling
|
||||
* OTA update workflows
|
||||
* AI and conversation systems
|
||||
* Cloud infrastructure and deployment
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
This project is part of the OpenJibo initiative and is not affiliated with the original Jibo company.
|
||||
|
||||
The mission is simple:
|
||||
|
||||
Bring Jibo back for everyone, technical or not.
|
||||
Make him what he was meant to be.
|
||||
Then we make him better.
|
||||
This folder now contains the first hosted scaffold, not just a README.
|
||||
|
||||
The intent is to grow from a runnable dev monolith into the real Azure deployment target without abandoning the existing abstractions work.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jibo.Cloud.Application\Jibo.Cloud.Application.csproj" />
|
||||
<ProjectReference Include="..\Jibo.Cloud.Infrastructure\Jibo.Cloud.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\Jibo.Runtime.Abstractions\Jibo.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
170
OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs
Normal file
170
OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.DependencyInjection;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddOpenJiboCloud();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (!context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
var webSocketService = context.RequestServices.GetRequiredService<JiboWebSocketService>();
|
||||
using var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
var kind = ResolveSocketKind(context.Request.Host.Host, context.Request.Path);
|
||||
var token = ResolveToken(context.Request);
|
||||
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
var received = await ReceiveAsync(socket, context.RequestAborted);
|
||||
if (received.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", context.RequestAborted);
|
||||
break;
|
||||
}
|
||||
|
||||
var envelope = new WebSocketMessageEnvelope
|
||||
{
|
||||
ConnectionId = Guid.NewGuid().ToString("N"),
|
||||
HostName = context.Request.Host.Host,
|
||||
Path = context.Request.Path.Value ?? "/",
|
||||
Kind = kind,
|
||||
Token = token,
|
||||
Text = received.MessageType == WebSocketMessageType.Text ? Encoding.UTF8.GetString(received.Buffer) : null,
|
||||
Binary = received.MessageType == WebSocketMessageType.Binary ? received.Buffer : null
|
||||
};
|
||||
|
||||
var replies = await webSocketService.HandleMessageAsync(envelope, context.RequestAborted);
|
||||
foreach (var reply in replies)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reply.Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes(reply.Text);
|
||||
await socket.SendAsync(payload, WebSocketMessageType.Text, true, context.RequestAborted);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/health", () => Results.Json(new { ok = true, service = "OpenJibo Cloud Api" }));
|
||||
|
||||
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var envelope = await BuildEnvelopeAsync(context, cancellationToken);
|
||||
var result = await service.DispatchAsync(envelope, cancellationToken);
|
||||
|
||||
context.Response.StatusCode = result.StatusCode;
|
||||
context.Response.ContentType = result.ContentType;
|
||||
|
||||
foreach (var header in result.Headers)
|
||||
{
|
||||
context.Response.Headers[header.Key] = header.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.BodyText))
|
||||
{
|
||||
await context.Response.WriteAsync(result.BodyText, cancellationToken);
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
return;
|
||||
|
||||
static async Task<ReceivedSocketMessage> ReceiveAsync(WebSocket socket, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await socket.ReceiveAsync(buffer, cancellationToken);
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
return new ReceivedSocketMessage(result.MessageType, ms.ToArray());
|
||||
}
|
||||
|
||||
static async Task<ProtocolEnvelope> BuildEnvelopeAsync(HttpContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Request.EnableBuffering();
|
||||
|
||||
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
||||
var bodyText = await reader.ReadToEndAsync(cancellationToken);
|
||||
context.Request.Body.Position = 0;
|
||||
|
||||
var target = context.Request.Headers["X-Amz-Target"].ToString();
|
||||
var targetParts = target.Split('.', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new ProtocolEnvelope
|
||||
{
|
||||
RequestId = Guid.NewGuid().ToString("N"),
|
||||
Transport = "http",
|
||||
Method = context.Request.Method,
|
||||
HostName = context.Request.Host.Host,
|
||||
Path = context.Request.Path.Value ?? "/",
|
||||
ServicePrefix = targetParts.Length > 0 ? targetParts[0] : null,
|
||||
Operation = targetParts.Length > 1 ? targetParts[1] : null,
|
||||
DeviceId = context.Request.Headers["X-Jibo-RobotId"].ToString(),
|
||||
CorrelationId = context.TraceIdentifier,
|
||||
FirmwareVersion = context.Request.Headers["X-OpenJibo-Firmware"].ToString(),
|
||||
ApplicationVersion = context.Request.Headers["X-OpenJibo-AppVersion"].ToString(),
|
||||
BodyText = bodyText,
|
||||
Headers = context.Request.Headers.ToDictionary(pair => pair.Key, pair => pair.Value.ToString(), StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
}
|
||||
|
||||
static string ResolveSocketKind(string host, PathString path)
|
||||
{
|
||||
if (host.Equals("api-socket.jibo.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "api-socket";
|
||||
}
|
||||
|
||||
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&
|
||||
path.StartsWithSegments("/v1/proactive"))
|
||||
{
|
||||
return "neo-hub-proactive";
|
||||
}
|
||||
|
||||
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "neo-hub-listen";
|
||||
}
|
||||
|
||||
return "openjibo";
|
||||
}
|
||||
|
||||
static string? ResolveToken(HttpRequest request)
|
||||
{
|
||||
var auth = request.Headers.Authorization.ToString();
|
||||
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return auth["Bearer ".Length..].Trim();
|
||||
}
|
||||
|
||||
var path = request.Path.Value;
|
||||
if (!string.IsNullOrWhiteSpace(path) && path.Length > 1)
|
||||
{
|
||||
return path.Trim('/');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal sealed record ReceivedSocketMessage(WebSocketMessageType MessageType, byte[] Buffer);
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Jibo.Cloud.Api": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:24604;http://localhost:24605"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
|
||||
namespace Jibo.Cloud.Application.Abstractions;
|
||||
|
||||
public interface ICloudStateStore
|
||||
{
|
||||
AccountProfile GetAccount();
|
||||
DeviceRegistration GetRobot();
|
||||
DeviceRegistration GetOrCreateDevice(string deviceId, string? firmwareVersion, string? applicationVersion);
|
||||
string IssueHubToken();
|
||||
string IssueRobotToken(string deviceId);
|
||||
CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path);
|
||||
CloudSession? FindSessionByToken(string token);
|
||||
IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null);
|
||||
UpdateManifest GetUpdateFrom(string? subsystem, string? fromVersion, string? filter);
|
||||
void UpdateRobot(DeviceRegistration registration);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jibo.Cloud.Domain\Jibo.Cloud.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\Jibo.Runtime.Abstractions\Jibo.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,62 @@
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public sealed class DemoConversationBroker : IConversationBroker
|
||||
{
|
||||
public Task<ResponsePlan> HandleTurnAsync(TurnContext turn, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var transcript = (turn.NormalizedTranscript ?? turn.RawTranscript ?? string.Empty).Trim();
|
||||
var lowered = transcript.ToLowerInvariant();
|
||||
|
||||
var reply = transcript.Length == 0
|
||||
? "I am listening."
|
||||
: lowered.Contains("time")
|
||||
? $"It is {DateTime.Now:hh:mm tt}."
|
||||
: lowered.Contains("hello") || lowered.Contains("hi")
|
||||
? "Hello from the OpenJibo cloud."
|
||||
: lowered.Contains("joke")
|
||||
? "Why did the robot bring a ladder? Because it wanted to reach the cloud."
|
||||
: $"I heard: {transcript}";
|
||||
|
||||
var plan = new ResponsePlan
|
||||
{
|
||||
SessionId = turn.SessionId,
|
||||
Status = ResponseStatus.Succeeded,
|
||||
IntentName = lowered.Contains("joke") ? "joke" : lowered.Contains("time") ? "time" : "chat",
|
||||
Topic = "conversation",
|
||||
DeviceId = turn.DeviceId,
|
||||
TargetHost = turn.HostName,
|
||||
DebugRoute = "demo-broker",
|
||||
Actions =
|
||||
{
|
||||
new SpeakAction
|
||||
{
|
||||
Sequence = 0,
|
||||
Text = reply,
|
||||
Voice = "griffin"
|
||||
},
|
||||
new ListenAction
|
||||
{
|
||||
Sequence = 1,
|
||||
Timeout = TimeSpan.FromSeconds(12),
|
||||
Mode = "follow-up"
|
||||
}
|
||||
},
|
||||
FollowUp = new FollowUpPolicy
|
||||
{
|
||||
KeepMicOpen = true,
|
||||
Timeout = TimeSpan.FromSeconds(12),
|
||||
ExpectedTopic = "conversation"
|
||||
},
|
||||
ProtocolMetadata = new Dictionary<string, object?>
|
||||
{
|
||||
["host"] = turn.HostName,
|
||||
["service"] = turn.ProtocolService,
|
||||
["operation"] = turn.ProtocolOperation
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(plan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.Text.Json;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
{
|
||||
private static readonly string[] AcceptedHosts =
|
||||
[
|
||||
"api.jibo.com",
|
||||
"openjibo.com",
|
||||
"openjibo.ai",
|
||||
"localhost"
|
||||
];
|
||||
|
||||
public Task<ProtocolDispatchResult> DispatchAsync(ProtocolEnvelope envelope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
||||
envelope.Path == "/" &&
|
||||
string.IsNullOrWhiteSpace(envelope.ServicePrefix))
|
||||
{
|
||||
return Task.FromResult(ProtocolDispatchResult.NoContent());
|
||||
}
|
||||
|
||||
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
||||
envelope.Path.Equals("/health", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(ProtocolDispatchResult.Ok(new { ok = true, host = envelope.HostName }));
|
||||
}
|
||||
|
||||
if (envelope.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) &&
|
||||
(envelope.Path.Equals("/upload/asr-binary", StringComparison.OrdinalIgnoreCase) ||
|
||||
envelope.Path.Equals("/upload/log-events", StringComparison.OrdinalIgnoreCase) ||
|
||||
envelope.Path.Equals("/upload/log-binary", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return Task.FromResult(ProtocolDispatchResult.Raw(200, string.Empty));
|
||||
}
|
||||
|
||||
if (!AcceptedHosts.Contains(envelope.HostName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
ok = true,
|
||||
accepted = false,
|
||||
host = envelope.HostName
|
||||
}));
|
||||
}
|
||||
|
||||
var servicePrefix = envelope.ServicePrefix ?? string.Empty;
|
||||
var operation = envelope.Operation ?? string.Empty;
|
||||
|
||||
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleAccount(operation));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Notification_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleNotification(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Loop_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleLoop(operation));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Robot_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleRobot(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Update_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleUpdate(operation, envelope));
|
||||
}
|
||||
|
||||
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
ok = true,
|
||||
service = servicePrefix,
|
||||
operation
|
||||
}));
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleAccount(string operation)
|
||||
{
|
||||
var account = stateStore.GetAccount();
|
||||
|
||||
return operation switch
|
||||
{
|
||||
"CreateHubToken" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
token = stateStore.IssueHubToken(),
|
||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||
}),
|
||||
"CreateAccessToken" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
token = $"access-{account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||
}),
|
||||
"Get" => ProtocolDispatchResult.Ok(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = account.Email,
|
||||
firstName = account.FirstName,
|
||||
lastName = account.LastName,
|
||||
accessKeyId = account.AccessKeyId,
|
||||
secretAccessKey = account.SecretAccessKey
|
||||
}
|
||||
}),
|
||||
_ => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = account.Email,
|
||||
firstName = account.FirstName,
|
||||
lastName = account.LastName
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleNotification(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
if (!operation.Equals("NewRobotToken", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
||||
}
|
||||
|
||||
var body = envelope.TryParseBody();
|
||||
var deviceId = envelope.DeviceId
|
||||
?? ReadString(body, "deviceId")
|
||||
?? ReadString(body, "serialNumber")
|
||||
?? "unknown-device";
|
||||
|
||||
stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);
|
||||
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
token = stateStore.IssueRobotToken(deviceId)
|
||||
});
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleLoop(string operation)
|
||||
{
|
||||
if (operation is not ("List" or "ListLoops"))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
var robot = stateStore.GetRobot();
|
||||
var account = stateStore.GetAccount();
|
||||
|
||||
return ProtocolDispatchResult.Ok(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = "openjibo-default-loop",
|
||||
name = "OpenJibo Default Loop",
|
||||
owner = account.AccountId,
|
||||
robot = robot.RobotId,
|
||||
robotFriendlyId = robot.DeviceId,
|
||||
members = Array.Empty<object>(),
|
||||
isSuspended = false,
|
||||
created = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
updated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleRobot(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
var robot = stateStore.GetRobot();
|
||||
|
||||
if (operation.Equals("UpdateRobot", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var updated = new DeviceRegistration
|
||||
{
|
||||
DeviceId = robot.DeviceId,
|
||||
RobotId = robot.RobotId,
|
||||
FriendlyName = robot.FriendlyName,
|
||||
FirmwareVersion = envelope.FirmwareVersion ?? robot.FirmwareVersion,
|
||||
ApplicationVersion = envelope.ApplicationVersion ?? robot.ApplicationVersion,
|
||||
HostMappings = robot.HostMappings
|
||||
};
|
||||
|
||||
stateStore.UpdateRobot(updated);
|
||||
robot = updated;
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = robot.RobotId,
|
||||
friendlyId = robot.DeviceId,
|
||||
name = robot.FriendlyName,
|
||||
firmwareVersion = robot.FirmwareVersion,
|
||||
applicationVersion = robot.ApplicationVersion
|
||||
});
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleUpdate(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
var body = envelope.TryParseBody();
|
||||
var subsystem = ReadString(body, "subsystem");
|
||||
var filter = ReadString(body, "filter");
|
||||
var fromVersion = ReadString(body, "fromVersion");
|
||||
|
||||
return operation switch
|
||||
{
|
||||
"ListUpdates" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
|
||||
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
|
||||
"GetUpdateFrom" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.GetUpdateFrom(subsystem, fromVersion, filter))),
|
||||
_ => ProtocolDispatchResult.Ok(Array.Empty<object>())
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapUpdate(UpdateManifest update)
|
||||
{
|
||||
return new
|
||||
{
|
||||
_id = update.UpdateId,
|
||||
created = update.CreatedUtc.ToUnixTimeMilliseconds(),
|
||||
fromVersion = update.FromVersion,
|
||||
toVersion = update.ToVersion,
|
||||
changes = update.Changes,
|
||||
url = update.Url,
|
||||
shaHash = update.ShaHash,
|
||||
length = update.Length,
|
||||
subsystem = update.Subsystem,
|
||||
filter = update.Filter
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!element.Value.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return property.ValueKind == JsonValueKind.String
|
||||
? property.GetString()
|
||||
: property.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Text.Json;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public sealed class JiboWebSocketService(
|
||||
ICloudStateStore stateStore,
|
||||
ProtocolToTurnContextMapper turnContextMapper,
|
||||
IConversationBroker conversationBroker,
|
||||
ResponsePlanToSocketMessagesMapper replyMapper)
|
||||
{
|
||||
public async Task<IReadOnlyList<WebSocketReply>> HandleMessageAsync(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var session = stateStore.FindSessionByToken(envelope.Token ?? string.Empty) ??
|
||||
stateStore.OpenSession(envelope.Kind, null, envelope.Token, envelope.HostName, envelope.Path);
|
||||
|
||||
if (envelope.IsBinary)
|
||||
{
|
||||
return
|
||||
[
|
||||
new WebSocketReply
|
||||
{
|
||||
Text = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "OPENJIBO_AUDIO_RECEIVED",
|
||||
data = new
|
||||
{
|
||||
bytes = envelope.Binary?.Length ?? 0,
|
||||
sessionId = session.SessionId
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var parsedType = ReadMessageType(envelope.Text);
|
||||
session.LastListenType = parsedType;
|
||||
|
||||
if (parsedType is "LISTEN" or "CLIENT_NLU" or "CLIENT_ASR")
|
||||
{
|
||||
var turn = turnContextMapper.MapListenMessage(envelope, session);
|
||||
var plan = await conversationBroker.HandleTurnAsync(turn, cancellationToken);
|
||||
|
||||
return replyMapper.Map(plan).Select(text => new WebSocketReply
|
||||
{
|
||||
Text = text
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
new WebSocketReply
|
||||
{
|
||||
Text = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "OPENJIBO_ACK",
|
||||
data = new
|
||||
{
|
||||
messageType = parsedType,
|
||||
sessionId = session.SessionId
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static string ReadMessageType(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(text);
|
||||
if (document.RootElement.TryGetProperty("type", out var type) && type.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return type.GetString() ?? "UNKNOWN";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "TEXT";
|
||||
}
|
||||
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public sealed class ProtocolToTurnContextMapper
|
||||
{
|
||||
public TurnContext MapListenMessage(WebSocketMessageEnvelope envelope, CloudSession session)
|
||||
{
|
||||
var text = ExtractTranscript(envelope.Text);
|
||||
|
||||
return new TurnContext
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
InputMode = session.LastListenType == "follow-up" ? TurnInputMode.FollowUp : TurnInputMode.DirectText,
|
||||
SourceKind = TurnSourceKind.Api,
|
||||
RawTranscript = text,
|
||||
NormalizedTranscript = text?.Trim(),
|
||||
DeviceId = session.DeviceId,
|
||||
HostName = envelope.HostName,
|
||||
RequestId = envelope.ConnectionId,
|
||||
ProtocolService = "neo-hub",
|
||||
ProtocolOperation = "listen",
|
||||
FirmwareVersion = session.Metadata.TryGetValue("firmwareVersion", out var firmwareVersion) ? firmwareVersion as string : null,
|
||||
ApplicationVersion = session.Metadata.TryGetValue("applicationVersion", out var applicationVersion) ? applicationVersion as string : null,
|
||||
IsFollowUpEligible = true
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ExtractTranscript(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(text);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (root.TryGetProperty("data", out var data))
|
||||
{
|
||||
if (data.TryGetProperty("text", out var transcript) && transcript.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return transcript.GetString();
|
||||
}
|
||||
|
||||
if (data.TryGetProperty("intent", out var intent) && intent.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return intent.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text.Json;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public sealed class ResponsePlanToSocketMessagesMapper
|
||||
{
|
||||
public IReadOnlyList<string> Map(ResponsePlan plan)
|
||||
{
|
||||
var speak = plan.Actions.OfType<SpeakAction>().FirstOrDefault();
|
||||
var messages = new List<string>();
|
||||
|
||||
if (speak is not null)
|
||||
{
|
||||
messages.Add(JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "OPENJIBO_RESPONSE",
|
||||
data = new
|
||||
{
|
||||
intent = plan.IntentName,
|
||||
text = speak.Text,
|
||||
followUpOpen = plan.FollowUp.KeepMicOpen,
|
||||
timeoutMs = (int)plan.FollowUp.Timeout.TotalMilliseconds
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
messages.Add(JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "EOS",
|
||||
data = new
|
||||
{
|
||||
sessionId = plan.SessionId
|
||||
}
|
||||
}));
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class AccountProfile
|
||||
{
|
||||
public string AccountId { get; init; } = "usr_openjibo_owner";
|
||||
public string Email { get; init; } = "owner@openjibo.local";
|
||||
public string FirstName { get; init; } = "Jibo";
|
||||
public string LastName { get; init; } = "Owner";
|
||||
public string AccessKeyId { get; init; } = "openjibo-access-key";
|
||||
public string SecretAccessKey { get; init; } = "openjibo-secret-access-key";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CapturedExchange
|
||||
{
|
||||
public string CaptureId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public DateTimeOffset CapturedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public ProtocolEnvelope Request { get; init; } = new();
|
||||
public ProtocolDispatchResult Response { get; init; } = ProtocolDispatchResult.Ok();
|
||||
public string Confidence { get; init; } = "observed";
|
||||
public IDictionary<string, string> Tags { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CloudSession
|
||||
{
|
||||
public string SessionId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string Kind { get; init; } = "http";
|
||||
public string? AccountId { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
public string? Token { get; init; }
|
||||
public string? HostName { get; init; }
|
||||
public string? Path { get; init; }
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset LastSeenUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string? LastListenType { get; set; }
|
||||
public string? LastTranscript { get; set; }
|
||||
public IDictionary<string, object?> Metadata { get; init; } = new Dictionary<string, object?>();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class DeviceRegistration
|
||||
{
|
||||
public string DeviceId { get; init; } = "my-robot-serial-number";
|
||||
public string RobotId { get; init; } = "my-robot-name";
|
||||
public string FriendlyName { get; init; } = "OpenJibo Dev Robot";
|
||||
public string? FirmwareVersion { get; init; }
|
||||
public string? ApplicationVersion { get; init; }
|
||||
public bool IsActive { get; init; } = true;
|
||||
public IDictionary<string, string> HostMappings { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class ProtocolDispatchResult
|
||||
{
|
||||
public int StatusCode { get; init; } = 200;
|
||||
public string ContentType { get; init; } = "application/x-amz-json-1.1";
|
||||
public string BodyText { get; init; } = "{}";
|
||||
public IDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static ProtocolDispatchResult Ok(object? body = null)
|
||||
{
|
||||
return new ProtocolDispatchResult
|
||||
{
|
||||
StatusCode = 200,
|
||||
BodyText = JsonSerializer.Serialize(body ?? new { ok = true })
|
||||
};
|
||||
}
|
||||
|
||||
public static ProtocolDispatchResult NoContent()
|
||||
{
|
||||
return new ProtocolDispatchResult
|
||||
{
|
||||
StatusCode = 204,
|
||||
BodyText = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
}
|
||||
|
||||
public static ProtocolDispatchResult Raw(int statusCode, string bodyText, string contentType = "text/plain")
|
||||
{
|
||||
return new ProtocolDispatchResult
|
||||
{
|
||||
StatusCode = statusCode,
|
||||
BodyText = bodyText,
|
||||
ContentType = contentType
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class ProtocolEnvelope
|
||||
{
|
||||
public string RequestId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public DateTimeOffset ReceivedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string Transport { get; init; } = "http";
|
||||
public string Method { get; init; } = "POST";
|
||||
public string HostName { get; init; } = "api.jibo.com";
|
||||
public string Path { get; init; } = "/";
|
||||
public string? ServicePrefix { get; init; }
|
||||
public string? Operation { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public string? FirmwareVersion { get; init; }
|
||||
public string? ApplicationVersion { get; init; }
|
||||
public string BodyText { get; init; } = string.Empty;
|
||||
public IDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public JsonElement? TryParseBody()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BodyText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(BodyText);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class UpdateManifest
|
||||
{
|
||||
public string UpdateId { get; init; } = "noop-update";
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string FromVersion { get; init; } = "unknown";
|
||||
public string ToVersion { get; init; } = "unknown";
|
||||
public string Changes { get; init; } = "No update available";
|
||||
public string Url { get; init; } = "https://api.jibo.com/update/noop";
|
||||
public string ShaHash { get; init; } = "noop";
|
||||
public long Length { get; init; }
|
||||
public string Subsystem { get; init; } = "robot";
|
||||
public string? Filter { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class UploadReference
|
||||
{
|
||||
public string UploadId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string Path { get; init; } = string.Empty;
|
||||
public string ContentType { get; init; } = "application/octet-stream";
|
||||
public long Length { get; init; }
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class WebSocketMessageEnvelope
|
||||
{
|
||||
public string ConnectionId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string HostName { get; init; } = string.Empty;
|
||||
public string Path { get; init; } = "/";
|
||||
public string Kind { get; init; } = "unknown";
|
||||
public string? Token { get; init; }
|
||||
public string? Text { get; init; }
|
||||
public byte[]? Binary { get; init; }
|
||||
public bool IsBinary => Binary is { Length: > 0 };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class WebSocketReply
|
||||
{
|
||||
public string? Text { get; init; }
|
||||
public bool Close { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddOpenJiboCloud(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ICloudStateStore, InMemoryCloudStateStore>();
|
||||
services.AddSingleton<IConversationBroker, DemoConversationBroker>();
|
||||
services.AddSingleton<ProtocolToTurnContextMapper>();
|
||||
services.AddSingleton<ResponsePlanToSocketMessagesMapper>();
|
||||
services.AddSingleton<JiboCloudProtocolService>();
|
||||
services.AddSingleton<JiboWebSocketService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jibo.Cloud.Application\Jibo.Cloud.Application.csproj" />
|
||||
<ProjectReference Include="..\Jibo.Cloud.Domain\Jibo.Cloud.Domain.csproj" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
{
|
||||
private readonly AccountProfile _account = new();
|
||||
private readonly ConcurrentDictionary<string, DeviceRegistration> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, CloudSession> _sessionsByToken = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<UpdateManifest> _updates;
|
||||
private DeviceRegistration _robot;
|
||||
|
||||
public InMemoryCloudStateStore()
|
||||
{
|
||||
_robot = new DeviceRegistration
|
||||
{
|
||||
HostMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["api.jibo.com"] = "openjibo.com",
|
||||
["api-socket.jibo.com"] = "openjibo.com",
|
||||
["neo-hub.jibo.com"] = "openjibo.com",
|
||||
["neohub.jibo.com"] = "openjibo.com"
|
||||
}
|
||||
};
|
||||
|
||||
_devices[_robot.DeviceId] = _robot;
|
||||
|
||||
_updates =
|
||||
[
|
||||
new UpdateManifest
|
||||
{
|
||||
UpdateId = "noop-update-robot",
|
||||
FromVersion = "unknown",
|
||||
ToVersion = "unknown",
|
||||
Changes = "No update available",
|
||||
Url = "https://api.jibo.com/update/noop",
|
||||
ShaHash = "noop",
|
||||
Subsystem = "robot"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public AccountProfile GetAccount() => _account;
|
||||
|
||||
public DeviceRegistration GetRobot() => _robot;
|
||||
|
||||
public DeviceRegistration GetOrCreateDevice(string deviceId, string? firmwareVersion, string? applicationVersion)
|
||||
{
|
||||
return _devices.AddOrUpdate(
|
||||
deviceId,
|
||||
_ => new DeviceRegistration
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
RobotId = $"robot-{deviceId}",
|
||||
FriendlyName = "OpenJibo Registered Robot",
|
||||
FirmwareVersion = firmwareVersion,
|
||||
ApplicationVersion = applicationVersion
|
||||
},
|
||||
(_, current) => new DeviceRegistration
|
||||
{
|
||||
DeviceId = current.DeviceId,
|
||||
RobotId = current.RobotId,
|
||||
FriendlyName = current.FriendlyName,
|
||||
FirmwareVersion = firmwareVersion ?? current.FirmwareVersion,
|
||||
ApplicationVersion = applicationVersion ?? current.ApplicationVersion,
|
||||
HostMappings = current.HostMappings
|
||||
});
|
||||
}
|
||||
|
||||
public string IssueHubToken()
|
||||
{
|
||||
var token = $"hub-{_account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
||||
_sessionsByToken[token] = new CloudSession
|
||||
{
|
||||
Kind = "hub",
|
||||
AccountId = _account.AccountId,
|
||||
Token = token,
|
||||
DeviceId = _robot.DeviceId
|
||||
};
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public string IssueRobotToken(string deviceId)
|
||||
{
|
||||
var token = $"token-{deviceId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
||||
_sessionsByToken[token] = new CloudSession
|
||||
{
|
||||
Kind = "robot",
|
||||
AccountId = _account.AccountId,
|
||||
Token = token,
|
||||
DeviceId = deviceId
|
||||
};
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path)
|
||||
{
|
||||
var session = new CloudSession
|
||||
{
|
||||
Kind = kind,
|
||||
AccountId = _account.AccountId,
|
||||
DeviceId = deviceId ?? _robot.DeviceId,
|
||||
Token = token,
|
||||
HostName = hostName,
|
||||
Path = path
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
_sessionsByToken[token] = session;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public CloudSession? FindSessionByToken(string token)
|
||||
{
|
||||
return _sessionsByToken.GetValueOrDefault(token);
|
||||
}
|
||||
|
||||
public IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null)
|
||||
{
|
||||
return _updates
|
||||
.Where(update => subsystem is null || update.Subsystem.Equals(subsystem, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(update => filter is null || string.Equals(update.Filter, filter, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public UpdateManifest GetUpdateFrom(string? subsystem, string? fromVersion, string? filter)
|
||||
{
|
||||
return ListUpdates(subsystem, filter).FirstOrDefault() ?? new UpdateManifest
|
||||
{
|
||||
UpdateId = $"noop-update-{subsystem ?? "robot"}-{fromVersion ?? "unknown"}",
|
||||
FromVersion = fromVersion ?? "unknown",
|
||||
ToVersion = fromVersion ?? "unknown",
|
||||
Filter = filter,
|
||||
Subsystem = subsystem ?? "robot"
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateRobot(DeviceRegistration registration)
|
||||
{
|
||||
_robot = registration;
|
||||
_devices[registration.DeviceId] = registration;
|
||||
}
|
||||
}
|
||||
@@ -1,253 +1,67 @@
|
||||
# Jibo.Cloud.Node
|
||||
|
||||
## Overview
|
||||
## Role
|
||||
|
||||
This folder contains the original **Node.js prototype** for the OpenJibo cloud layer.
|
||||
This folder contains the protocol oracle for OpenJibo.
|
||||
|
||||
This implementation started as a fast way to stand up a working "fake cloud" so Jibo could begin talking to a replacement backend again. It has been used to map behavior, discover endpoints, observe payloads, and validate real interactions with a live robot.
|
||||
The Node server is still the best place to:
|
||||
|
||||
This is the experimental proving ground for the broader `Jibo.Cloud` effort.
|
||||
- observe how a real Jibo talks to the cloud
|
||||
- discover endpoints, payloads, headers, and timing
|
||||
- validate hypotheses quickly against hardware
|
||||
- produce sanitized fixtures for the .NET port
|
||||
|
||||
---
|
||||
It is no longer the intended production runtime.
|
||||
|
||||
## Purpose
|
||||
## What Stays Here
|
||||
|
||||
The goals of this Node implementation are:
|
||||
- reverse-engineering work
|
||||
- protocol discovery
|
||||
- capture and replay fixture generation
|
||||
- narrow experiments needed to unblock the .NET hosted cloud
|
||||
|
||||
* Reverse engineer Jibo cloud behavior
|
||||
* Recreate enough of the original cloud to restore functionality
|
||||
* Capture real request and response data
|
||||
* Prototype OTA update delivery paths
|
||||
* Validate speech, jokes, and interaction flows
|
||||
* Serve as a reference for the C# / .NET implementation
|
||||
## What Moves Out
|
||||
|
||||
---
|
||||
- production hosting concerns
|
||||
- long-term storage and deployment architecture
|
||||
- hardened runtime orchestration
|
||||
- Azure-facing operational concerns
|
||||
|
||||
Those belong in `../dotnet`.
|
||||
|
||||
## Current Capabilities
|
||||
|
||||
This server currently supports:
|
||||
The prototype already demonstrates a meaningful slice of the old cloud:
|
||||
|
||||
* HTTPS API handling with `X-Amz-Target` routing
|
||||
* WebSocket connections for Jibo communication
|
||||
* Token/session handling (prototype-level)
|
||||
* Account and robot identity flows (mocked)
|
||||
* Media, loop, and key endpoints (partial)
|
||||
* OTA update endpoints (in progress)
|
||||
* Speech pipeline using:
|
||||
- HTTPS API routing by `X-Amz-Target`
|
||||
- WebSocket handling for Jibo communication paths
|
||||
- token and session bootstrapping
|
||||
- account, loop, robot, key, media, and update operations
|
||||
- ASR-oriented audio handling with `ffmpeg` and `whisper.cpp`
|
||||
- synthetic turn handling for greetings, time, jokes, and basic chat
|
||||
- extensive request and WebSocket logging for protocol discovery
|
||||
|
||||
* Ogg normalization
|
||||
* ffmpeg conversion
|
||||
* whisper.cpp transcription
|
||||
* Basic intent handling (jokes, greetings, time, etc.)
|
||||
* Skill action responses (speech + simple animations)
|
||||
## Fixture Workflow
|
||||
|
||||
---
|
||||
Use this implementation as the source of truth for replay fixtures.
|
||||
|
||||
## package.json
|
||||
- capture observed request and response pairs
|
||||
- sanitize account ids, emails, tokens, hostnames, and secrets
|
||||
- save fixtures under [fixtures](C:/Projects/JiboExperiments/OpenJibo/src/Jibo.Cloud/node/fixtures)
|
||||
- use those fixtures to drive the .NET compatibility port
|
||||
|
||||
This project uses a minimal Node setup:
|
||||
## Real Device Reality
|
||||
|
||||
```json
|
||||
:contentReference[oaicite:0]{index=0}
|
||||
```
|
||||
Today, a real Jibo still needs a controlled environment to talk to this server.
|
||||
|
||||
Install dependencies with:
|
||||
That means:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
- controlled router or DNS interception
|
||||
- redirection of legacy Jibo hosts
|
||||
- RCM/device modification for TLS or host validation where required
|
||||
|
||||
---
|
||||
That reality is documented in [device-bootstrap.md](C:/Projects/JiboExperiments/OpenJibo/docs/device-bootstrap.md). OTA is a future improvement path, not the current bootstrap dependency.
|
||||
|
||||
## Running the Server
|
||||
|
||||
### Requirements
|
||||
|
||||
* Node.js
|
||||
* `ws` package
|
||||
* `ffmpeg`
|
||||
* `whisper.cpp` + model
|
||||
* TLS certificate and key
|
||||
|
||||
### TLS Files
|
||||
|
||||
Place in working directory:
|
||||
|
||||
```plaintext
|
||||
cert.pem
|
||||
key.pem
|
||||
```
|
||||
|
||||
### Environment Variables (optional)
|
||||
|
||||
```bash
|
||||
FFMPEG_BIN=/usr/bin/ffmpeg
|
||||
WHISPER_CPP_BIN=/path/to/whisper-cli
|
||||
WHISPER_MODEL=/path/to/model.bin
|
||||
```
|
||||
|
||||
### Start
|
||||
|
||||
```bash
|
||||
node open-jibo-link.js
|
||||
```
|
||||
|
||||
Server listens on:
|
||||
|
||||
```
|
||||
https://0.0.0.0:443
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Jibo to Talk to This Server
|
||||
|
||||
This is the part that matters most.
|
||||
|
||||
Jibo does not naturally connect to a custom server, so you need to control its network environment and TLS behavior.
|
||||
|
||||
### Network Setup (Mango Travel Router)
|
||||
|
||||
A simple and effective approach:
|
||||
|
||||
* Use a **Mango travel router (~$30)**
|
||||
* Connect Jibo to this network
|
||||
* Block outbound internet access
|
||||
* Force DNS resolution to your server
|
||||
|
||||
### DNS Control
|
||||
|
||||
On the router:
|
||||
|
||||
* Map the following domains to your server:
|
||||
|
||||
```
|
||||
api.jibo.com
|
||||
api-socket.jibo.com
|
||||
neohub.jibo.com
|
||||
```
|
||||
|
||||
* Intercept Google DNS requests (hardcoded in Jibo):
|
||||
|
||||
```
|
||||
8.8.8.8
|
||||
8.8.4.4
|
||||
```
|
||||
|
||||
These must be redirected or blocked so Jibo cannot bypass your DNS.
|
||||
|
||||
---
|
||||
|
||||
## TLS / Certificate Handling
|
||||
|
||||
Jibo expects valid TLS and will reject unknown/self-signed certificates by default.
|
||||
|
||||
Because of the older Node runtime and native binaries used on Jibo, this cannot be fully bypassed at the system level.
|
||||
|
||||
### Required Changes
|
||||
|
||||
You must modify Jibo’s runtime to disable certificate validation:
|
||||
|
||||
* Update Node-based modules to allow self-signed certs
|
||||
* Modify any code using:
|
||||
|
||||
```js
|
||||
rejectUnauthorized: true
|
||||
```
|
||||
|
||||
Change to:
|
||||
|
||||
```js
|
||||
rejectUnauthorized: false
|
||||
```
|
||||
|
||||
* Patch any native or binary services that enforce TLS validation
|
||||
* Set environment variables where possible to disable strict SSL
|
||||
|
||||
This typically requires:
|
||||
|
||||
* RCM access to Jibo
|
||||
* Direct file modification on the device
|
||||
|
||||
---
|
||||
|
||||
## WiFi Setup (QR Code)
|
||||
|
||||
Jibo connects to WiFi using a QR code.
|
||||
|
||||
You can generate one here:
|
||||
|
||||
https://kevinblog.sytes.net/Jibo/WifiGenerator/
|
||||
|
||||
This allows you to easily connect Jibo to your controlled network (such as the Mango router).
|
||||
|
||||
---
|
||||
|
||||
## OTA Update Direction
|
||||
|
||||
One of the most important long-term strategies is leveraging Jibo’s built-in OTA update mechanism.
|
||||
|
||||
This server already includes update-related endpoints to support:
|
||||
|
||||
* Skill delivery
|
||||
* Update metadata handling
|
||||
* Future system updates
|
||||
|
||||
The goal is to eventually:
|
||||
|
||||
* Deliver updates without requiring device hacking
|
||||
* Push new functionality directly through Jibo’s native update flow
|
||||
* Provide a simple recovery path for non-technical users
|
||||
|
||||
---
|
||||
|
||||
## Logging and Observability
|
||||
|
||||
This server is heavily instrumented for debugging and discovery.
|
||||
|
||||
It logs:
|
||||
|
||||
* Incoming requests and headers
|
||||
* Target routing (`X-Amz-Target`)
|
||||
* Responses
|
||||
* WebSocket activity
|
||||
* Audio processing stages
|
||||
* Transcription results
|
||||
|
||||
This makes it both a working cloud stub and a reverse engineering tool.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
This is still a prototype:
|
||||
|
||||
* Many endpoints are partial or mocked
|
||||
* No persistent storage
|
||||
* Security is minimal
|
||||
* Configuration is partially hardcoded
|
||||
* Designed for experimentation, not production
|
||||
|
||||
---
|
||||
|
||||
## Future Direction
|
||||
|
||||
This implementation will evolve into:
|
||||
|
||||
* A full C# / .NET cloud service
|
||||
* Azure-hosted infrastructure
|
||||
* Trusted SSL with real domains
|
||||
* Clean OTA update pipeline
|
||||
* Integration with OpenJibo runtime
|
||||
|
||||
The Node version will remain as:
|
||||
|
||||
* A reference implementation
|
||||
* A fast experimentation environment
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
This project is part of the OpenJibo effort and is not affiliated with the original Jibo company.
|
||||
## Next Job
|
||||
|
||||
The main job of this folder now is to keep the .NET port honest.
|
||||
|
||||
10
OpenJibo/src/Jibo.Cloud/node/fixtures/README.md
Normal file
10
OpenJibo/src/Jibo.Cloud/node/fixtures/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Node Fixtures
|
||||
|
||||
These fixtures are sanitized captures derived from the Node protocol oracle and are intended to seed compatibility testing for the .NET port.
|
||||
|
||||
Current fixture groups:
|
||||
|
||||
- `http/`
|
||||
Basic `X-Amz-Target` request and response examples for startup flows.
|
||||
|
||||
Expand this folder whenever new robot traffic is captured and cleaned.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"host": "api.jibo.com",
|
||||
"method": "POST",
|
||||
"path": "/",
|
||||
"headers": {
|
||||
"x-amz-target": "Account_20160715.CreateHubToken"
|
||||
},
|
||||
"body": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"statusCode": 200,
|
||||
"body": {
|
||||
"token": "hub-usr_test_001-1712700000000",
|
||||
"expires": 1712703600000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"host": "api.jibo.com",
|
||||
"method": "POST",
|
||||
"path": "/",
|
||||
"headers": {
|
||||
"x-amz-target": "Notification_20160715.NewRobotToken"
|
||||
},
|
||||
"body": {
|
||||
"deviceId": "my-robot-serial-number"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"statusCode": 200,
|
||||
"body": {
|
||||
"token": "token-my-robot-serial-number-1712700000000"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ public sealed class ResponsePlan
|
||||
|
||||
public string? IntentName { get; init; }
|
||||
public string? Topic { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
public string? TargetHost { get; init; }
|
||||
public IList<PlanAction> Actions { get; init; } = new List<PlanAction>();
|
||||
|
||||
public FollowUpPolicy FollowUp { get; init; } = FollowUpPolicy.None;
|
||||
@@ -15,4 +17,5 @@ public sealed class ResponsePlan
|
||||
|
||||
public string? DebugRoute { get; init; }
|
||||
public IDictionary<string, object?> Diagnostics { get; init; } = new Dictionary<string, object?>();
|
||||
public IDictionary<string, object?> ProtocolMetadata { get; init; } = new Dictionary<string, object?>();
|
||||
}
|
||||
@@ -9,6 +9,13 @@ public sealed class RobotEvent
|
||||
public string? SessionId { get; init; }
|
||||
public string? Transcript { get; init; }
|
||||
public string? WakePhrase { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
public string? HostName { get; init; }
|
||||
public string? RequestId { get; init; }
|
||||
public string? ProtocolService { get; init; }
|
||||
public string? ProtocolOperation { get; init; }
|
||||
public string? FirmwareVersion { get; init; }
|
||||
public string? ApplicationVersion { get; init; }
|
||||
|
||||
public IDictionary<string, object?> Payload { get; init; } = new Dictionary<string, object?>();
|
||||
}
|
||||
@@ -12,6 +12,13 @@ public sealed class TurnContext
|
||||
public string? WakePhrase { get; init; }
|
||||
public string? RawTranscript { get; init; }
|
||||
public string? NormalizedTranscript { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
public string? HostName { get; init; }
|
||||
public string? RequestId { get; init; }
|
||||
public string? ProtocolService { get; init; }
|
||||
public string? ProtocolOperation { get; init; }
|
||||
public string? FirmwareVersion { get; init; }
|
||||
public string? ApplicationVersion { get; init; }
|
||||
|
||||
public string? Locale { get; init; } = "en-US";
|
||||
public string? TimeZone { get; init; }
|
||||
|
||||
48
OpenJibo/src/OpenJibo.Site/index.html
Normal file
48
OpenJibo/src/OpenJibo.Site/index.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenJibo</title>
|
||||
<link rel="stylesheet" href="site.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">OpenJibo</p>
|
||||
<h1>Bringing Jibo back with a stable open cloud.</h1>
|
||||
<p class="lede">
|
||||
OpenJibo is rebuilding the hosted layer Jibo still expects, then using that foothold
|
||||
to modernize the platform step by step.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a href="https://github.com/" class="button primary">Source Repos</a>
|
||||
<a href="../../docs/device-bootstrap.md" class="button secondary">Bootstrap Docs</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Current Direction</h2>
|
||||
<p>
|
||||
The first milestone is a stable Azure-hosted replacement cloud. We support real robots
|
||||
first through controlled DNS plus targeted device patching, then smooth the path later with OTA.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<article class="card">
|
||||
<h3>Cloud First</h3>
|
||||
<p>Replace the missing hosted services before attempting deeper on-device modernization.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h3>Real Hardware</h3>
|
||||
<p>Use repeatable device bootstrap steps instead of pretending recovery is already one-click.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h3>Open Path</h3>
|
||||
<p>Keep the protocol work, docs, and codebase visible so the community can iterate with us.</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
120
OpenJibo/src/OpenJibo.Site/site.css
Normal file
120
OpenJibo/src/OpenJibo.Site/site.css
Normal file
@@ -0,0 +1,120 @@
|
||||
:root {
|
||||
--bg: #f2ebdf;
|
||||
--ink: #1b2d2a;
|
||||
--accent: #c45b2f;
|
||||
--accent-dark: #8f3d1c;
|
||||
--panel: rgba(255, 250, 242, 0.78);
|
||||
--line: rgba(27, 45, 42, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(196, 91, 47, 0.16), transparent 35%),
|
||||
radial-gradient(circle at bottom right, rgba(27, 45, 42, 0.12), transparent 30%),
|
||||
linear-gradient(180deg, #fbf7ef 0%, var(--bg) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1080px, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 3rem 0 4rem;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.panel,
|
||||
.card {
|
||||
backdrop-filter: blur(8px);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 18px 48px rgba(41, 33, 22, 0.08);
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 0.75rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-dark);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.5rem, 6vw, 5rem);
|
||||
line-height: 0.94;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 42rem;
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 0.9rem 1.2rem;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: var(--accent);
|
||||
color: #fffaf4;
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1.5rem 1.75rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.4rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hero {
|
||||
padding: 2rem 1.4rem;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.card {
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
12
README.md
12
README.md
@@ -1,3 +1,13 @@
|
||||
# JiboExperiments
|
||||
|
||||
A place to put code that isn't quite OS worthy, but needs a home.
|
||||
Experiments, protocol notes, and early implementation work for bringing Jibo back.
|
||||
|
||||
The active work lives in [OpenJibo](C:/Projects/JiboExperiments/OpenJibo), where the current direction is:
|
||||
|
||||
- build a stable Azure-hosted replacement cloud first
|
||||
- use the Node prototype as the reverse-engineering oracle
|
||||
- port the real hosted system to .NET
|
||||
- support first devices through RCM plus controlled DNS/TLS patching
|
||||
- use OTA later as the path to a smoother recovery experience
|
||||
|
||||
This repo is intentionally a working lab, not a finished product.
|
||||
|
||||
Reference in New Issue
Block a user