Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/tanstack-router/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ conditionally mounted for dev use `env.isDev` from `@/shared/config/env`.
## Router config

`apps/web/src/routes/router.tsx` creates the router. It mounts under the proxy sub-path via
`BASE_PREFIX` (from `@/shared/lib/basePath`) so client routing resolves behind the oasis pod-proxy:
`BASE_PREFIX` (from `@/shared/lib/basePath`) so client routing resolves behind the tangle pod-proxy:

```typescript
export const router = createRouter({
Expand Down
20 changes: 10 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Minerva (Shopify SSO) auth config for scripts/minervaAuth.ts.
# Copy to .env (gitignored) and fill in the internal values.
# Oktasso SSO auth config for scripts/oktassoAuth.ts.
# Copy to .env (gitignored) and fill in the provider values.

# Shopify-internal endpoints + SSO client (ask your team / see tangle_deploy).
MINERVA_OAUTH_ENDPOINT_BASE=
MINERVA_TOKEN_EXCHANGE_URL=
MINERVA_CLIENT_ID=
# Provider endpoints + SSO client for your deployment.
OKTASSO_OAUTH_ENDPOINT_BASE=
OKTASSO_TOKEN_EXCHANGE_URL=
OKTASSO_CLIENT_ID=

# Generic OAuth/PKCE settings — the defaults below are fine for local dev.
MINERVA_SCOPES="profile email openid"
MINERVA_REDIRECT_URI=http://localhost:3001/auth
MINERVA_CALLBACK_PORT=3001
OKTASSO_SCOPES="profile email openid"
OKTASSO_REDIRECT_URI=http://localhost:3001/auth
OKTASSO_CALLBACK_PORT=3001
# Token cache lifetime in ms (55 minutes).
MINERVA_REFRESH_AFTER_MS=3300000
OKTASSO_REFRESH_AFTER_MS=3300000

# Default bundle used by the Sessions page New session button.
VITE_DEFAULT_SESSION_BUNDLE_ID=tangle
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ the full list and defaults):
| `PI_BIN` | Path to the `pi` agent executable |
| `PI_PROVIDER` / `PI_MODEL` / `PI_THINKING` | Default LLM provider, model, and thinking level |
| `PI_PROXY_URL` / `PI_PROXY_API_KEY` | LLM proxy endpoint and credential |
| `TANGLE_API_URL` / `OASIS_TOKEN` | Tangle pipeline API for bundles that integrate with it |
| `TANGLE_API_URL` / `TANGLE_TOKEN` | Tangle pipeline API for bundles that integrate with it |

### Common commands

Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"prettier": "@tangent/build/prettier",
"scripts": {
"dev": "AUTH_JWT_TOKEN_COOKIE_NAME=MINERVA_TOKEN tsx watch --tsconfig ./tsconfig.json src/index.ts",
"dev": "AUTH_JWT_TOKEN_COOKIE_NAME=OKTASSO_TOKEN tsx watch --tsconfig ./tsconfig.json src/index.ts",
"seed": "tsx --tsconfig ./tsconfig.json src/seed.ts",
"build": "node build.mjs",
"lint": "eslint .",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/auth/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function pickString(

/**
* Resolves the current {@link UserIdentity} from a raw `Cookie` header. Reads
* the Minerva JWT from {@link AUTH_JWT_TOKEN_COOKIE_NAME}, decodes its payload
* the Oktasso JWT from {@link AUTH_JWT_TOKEN_COOKIE_NAME}, decodes its payload
* (no signature check), and maps the email + name claims onto the identity.
*
* Returns `null` when the cookie name is unconfigured, the cookie is missing,
Expand Down
18 changes: 9 additions & 9 deletions apps/server/src/bundleUi/egressAllowlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A sandboxed component can only reach destinations registered here; anything
* else is denied before a network call is made. Unlike the earlier alias-based
* stub, components now name a **real, full URL** (e.g. the Oasis executions API)
* stub, components now name a **real, full URL** (e.g. the Tangle executions API)
* and the proxy validates it against the allowlist of host/path patterns, then
* performs the actual `fetch` server-side, injecting any credentials and
* stripping host internals out of the response.
Expand Down Expand Up @@ -42,14 +42,14 @@ interface EgressRule {
}

/**
* Optional credential for the Oasis API, injected server-side as a `Cookie`
* Optional credential for the Tangle API, injected server-side as a `Cookie`
* header. Unset by default (the public executions-state endpoint is
* unauthenticated); set `OASIS_TOKEN` to a cookie string such as
* `MINERVA_TOKEN=<session>` to authenticate. Generate one with
* `pnpm oasis:token` (or `pnpm dev:auth`).
* unauthenticated); set `TANGLE_TOKEN` to a cookie string such as
* `OKTASSO_TOKEN=<session>` to authenticate. Generate one with
* `pnpm tangle:token` (or `pnpm dev:auth`).
*/
function oasisAuthHeaders(): Record<string, string> {
const token = process.env.OASIS_TOKEN;
function tangleAuthHeaders(): Record<string, string> {
const token = process.env.TANGLE_TOKEN;
return token ? { cookie: token } : {};
}

Expand Down Expand Up @@ -91,7 +91,7 @@ const TANGLE_RULES: EgressRule[] = TANGLE_PATH_RULES.map((rule) => ({
method: rule.method,
matches: (url) =>
url.origin === TANGLE_API_ORIGIN && rule.test.test(url.pathname),
headers: oasisAuthHeaders,
headers: tangleAuthHeaders,
}));

/** Registered destinations the bridge may reach. */
Expand All @@ -103,7 +103,7 @@ const EGRESS_RULES: EgressRule[] = [
matches: (url) =>
url.origin === "https://oasis.shopify.io" &&
/^\/api\/executions\/[^/]+\/state$/.test(url.pathname),
headers: oasisAuthHeaders,
headers: tangleAuthHeaders,
},
...TANGLE_RULES,
];
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const INTERNAL_TOKEN =
process.env.TANGENT_INTERNAL_TOKEN ?? randomUUID();

/**
* Name of the cookie holding the Minerva JWT that `GET /api/me` reads to resolve
* Name of the cookie holding the Oktasso JWT that `GET /api/me` reads to resolve
* the current user. Empty by default so the route is effectively disabled until
* an environment supplies the cookie name (the local `dev` script sets it).
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ app.use(
);
app.use("/api/agent-bundles", createAgentBundlesRouter(agentBundleStore));
app.use("/api/global-memory", createGlobalMemoryRouter(memory));
// Returns the current user, derived from the Minerva JWT cookie.
// Returns the current user, derived from the Oktasso JWT cookie.
app.use("/api/me", createMeRouter());
// Internal API for the orchestrator extension running inside each Pi process.
app.use("/internal/agents", createInternalAgentsRouter(store, pi));
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/pi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export interface SessionAgents {
*/
config?: ResolvedSessionConfig;
/**
* The human who owns the session, resolved from their Minerva JWT. Captured
* The human who owns the session, resolved from their Oktasso JWT. Captured
* so every agent's system prompt can be told who it's helping. Absent for
* unauthenticated sessions.
*/
Expand Down
8 changes: 4 additions & 4 deletions apps/server/src/routes/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { resolveUserIdentity } from "../auth/identity.ts";
import { AUTH_JWT_TOKEN_COOKIE_NAME } from "../config.ts";

/**
* Handles `GET /api/me`. Resolves the current user from the Minerva JWT in the
* Handles `GET /api/me`. Resolves the current user from the Oktasso JWT in the
* configured cookie ({@link AUTH_JWT_TOKEN_COOKIE_NAME}) and returns the full
* identity. `user_id` is kept (set to the email) for backward compatibility
* alongside the structured `email` / `first_name` / `last_name` fields.
*/
function handleGetMe(req: Request, res: Response): void {
if (!AUTH_JWT_TOKEN_COOKIE_NAME) {
res.status(501).json({ error: "Minerva cookie name not configured" });
res.status(501).json({ error: "Oktasso cookie name not configured" });
return;
}

Expand All @@ -25,8 +25,8 @@ function handleGetMe(req: Request, res: Response): void {
}

/**
* Public REST router exposing the current user, derived from the Minerva JWT in
* the `MINERVA_TOKEN` cookie. The signature is not verified at this time.
* Public REST router exposing the current user, derived from the Oktasso JWT in
* the `OKTASSO_TOKEN` cookie. The signature is not verified at this time.
*/
export function createMeRouter(): Router {
const router = Router();
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/routes/sessions/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export async function handleCreateSession(
return;
}

// Resolve the creator's identity from their Minerva JWT cookie so every agent
// Resolve the creator's identity from their Oktasso JWT cookie so every agent
// spawned for the session knows who it's helping.
const user = resolveUserIdentity(req.headers.cookie) ?? undefined;
const session = await store.createSession({ name: body.name, user });
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/store/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import type {
*/
export type SessionAgentStatus = "active" | "killed" | "error";

/**
* Input accepted by {@link SessionStore.createSession}: the public wire request
* plus the server-resolved {@link UserIdentity} (from the creator's Oktasso JWT),
* which is never part of the client-supplied body.
*/
export interface CreateSessionParams {
name?: string;
user?: UserIdentity;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<script>
// Set <base href> to the proxy mount root (e.g. ".../ports/8000/") so the
// app works behind the oasis pod-proxy sub-path. The origin server cannot
// app works behind the tangle pod-proxy sub-path. The origin server cannot
// know the external prefix, so it is derived here from the browser URL.
// Must run before any asset tag so their relative URLs resolve correctly.
(function () {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/chat/hooks/useSessionChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function useSessionChat(sessionId: string) {
if (!sessionId) return;

// Connects to the same origin; Vite proxies /socket.io to the dev server.
// The path is mount-prefix aware so it works behind the oasis pod-proxy
// The path is mount-prefix aware so it works behind the tangle pod-proxy
// sub-path (`${BASE_PREFIX}socket.io`, i.e. `/socket.io` at the origin root).
const socket = io({ autoConnect: true, path: `${BASE_PREFIX}socket.io` });
socketRef.current = socket;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/user/api/userApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DEFAULT_USER } from "../model/userDisplay";

/**
* Fetches the current user from `GET /api/me`. The endpoint returns `401`/`501`
* when no Minerva JWT is present (e.g. local development), so any non-ok
* when no Oktasso JWT is present (e.g. local development), so any non-ok
* response resolves to {@link DEFAULT_USER} rather than throwing — the UI always
* has an identity to render.
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/user/model/userDisplay.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UserIdentity } from "@tangent/shared/contracts";

/**
* Safety-net identity used when the Minerva JWT is unavailable (e.g. local
* Safety-net identity used when the Oktasso JWT is unavailable (e.g. local
* development without the cookie configured), so the UI always has a name to
* show.
*/
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/routes/bundle-ui-harness/BundleUiHarnessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function BundleUiHarnessPage() {
const [crashToken, setCrashToken] = useState(0);
const [showCrash, setShowCrash] = useState(false);
const [collapsed, setCollapsed] = useState(false);
// A sample Oasis execution id; the component polls the real allowlisted
// A sample Tangle execution id; the component polls the real allowlisted
// endpoint and degrades to a loading state when the host is unreachable.
const [executionId] = useState("019ea56d72cd5f4d75f6");

Expand All @@ -36,7 +36,7 @@ export function BundleUiHarnessPage() {
<Heading level={2}>Bundle UI harness</Heading>
<Paragraph tone="subdued">
Renders a sandboxed component in a Web Worker via remote-dom. The
progress bar polls the real allowlisted egress endpoint (Oasis
progress bar polls the real allowlisted egress endpoint (Tangle
execution state); the button sends a prompt through the host bridge.
</Paragraph>
</BlockStack>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/routes/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const router = createRouter({
routeTree,
history: createBrowserHistory(),
// Mount the router under the proxy sub-path so client-side routing resolves
// relative to the oasis pod-proxy prefix (`/` at the origin root).
// relative to the tangle pod-proxy prefix (`/` at the origin root).
basepath: BASE_PREFIX,
defaultPreload: "intent",
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/shared/lib/basePath.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Proxy mount-prefix awareness.
*
* Behind the oasis pod-proxy the app is served from a sub-path
* Behind the tangle pod-proxy the app is served from a sub-path
* (e.g. `.../ports/8000/`), not the origin root. `index.html` injects a
* `<base href>` pointing at that mount root, so `document.baseURI` reflects it.
* `BASE_PREFIX` captures that prefix once at boot and `apiUrl` rewrites
Expand Down
2 changes: 1 addition & 1 deletion apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const apiTarget = process.env.API_TARGET ?? "http://localhost:8787";
// https://vite.dev/config/
export default defineConfig({
// Relative asset base so the built index.html references assets as
// "./assets/..." rather than "/assets/...". Behind the oasis pod-proxy the
// "./assets/..." rather than "/assets/...". Behind the tangle pod-proxy the
// Kubernetes apiserver rewrites same-host absolute-path URLs in HTML to its
// own proxy path; relative URLs are left untouched and resolve against the
// runtime-injected <base href> (see index.html).
Expand Down
4 changes: 2 additions & 2 deletions docs/bundle-ui/authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ renders a progress chip:
import { BlockStack, Card, host, Progress, Text } from "@tangent/bundle-ui";
import { useEffect, useState } from "react";

const OASIS_BASE = "https://oasis.shopify.io/api/executions";
const TANGLE_BASE = "https://tangle.example.com/api/executions";

export default function PipelineProgress() {
const [executionId, setExecutionId] = useState<string | null>(null);
Expand All @@ -79,7 +79,7 @@ export default function PipelineProgress() {
const tick = async () => {
try {
// A real, allowlisted endpoint — the host proxy validates the URL.
const res = await host.fetch(`${OASIS_BASE}/${executionId}/state`);
const res = await host.fetch(`${TANGLE_BASE}/${executionId}/state`);
if (active && res.ok) {
const s = (res.json as { child_execution_status_summary?: unknown })
?.child_execution_status_summary as
Expand Down
10 changes: 5 additions & 5 deletions docs/bundle-ui/host-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ without growing the bridge surface.
await host.execUICommand({ type: "collapse" });
await host.execUICommand({
type: "openUrl",
url: "https://oasis.shopify.io/runs/abc",
url: "https://tangle.example.com/runs/abc",
});
```

Expand Down Expand Up @@ -130,7 +130,7 @@ interface HostResponse {

- `input` is a **real, absolute `https://` URL** for the destination. The proxy
matches it against allowlisted host/path patterns; authors name the actual
endpoint (e.g. `https://oasis.shopify.io/api/executions/<id>/state`) rather
endpoint (e.g. `https://tangle.example.com/api/executions/<id>/state`) rather
than a logical alias.
- The proxy **rejects any destination not on the allowlist** before making a
network call, and strips/normalizes headers in both directions. Credentials
Expand All @@ -151,10 +151,10 @@ const EGRESS_RULES = [
{
method: "GET",
matches: (url) =>
url.origin === "https://oasis.shopify.io" &&
url.origin === "https://tangle.example.com" &&
/^\/api\/executions\/[^/]+\/state$/.test(url.pathname),
// optional bearer token attached server-side; never exposed to the worker
headers: oasisAuthHeaders,
headers: tangleAuthHeaders,
},
];
```
Expand All @@ -171,7 +171,7 @@ Request from the component:
```ts
const id = "019ea56d72cd5f4d75f6";
const res = await host.fetch(
`https://oasis.shopify.io/api/executions/${id}/state`,
`https://tangle.example.com/api/executions/${id}/state`,
);
if (!res.ok) throw new Error(`status ${res.status}`);
const summary = (res.json as { child_execution_status_summary: unknown })
Expand Down
24 changes: 12 additions & 12 deletions docs/server/egress-and-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ sequenceDiagram
participant Caller as Agent tool / UI component
participant Route as Egress route
participant Resolver as resolveEgress
participant Upstream as Tangle / Oasis
participant Upstream as Tangle

Caller->>Route: POST egress { input: URL, init }
activate Route
Expand Down Expand Up @@ -106,7 +106,7 @@ Allowlist properties:
- Only `http(s)` URLs whose method + parsed URL match a registered `EgressRule`
are permitted; everything else throws `EgressDeniedError` (403).
- Rules cover the configured Tangle API origin (`TANGLE_API_URL`) for specific
`pipeline_runs` / `executions` / `artifacts` paths, plus the Oasis execution
`pipeline_runs` / `executions` / `artifacts` paths, plus the Tangle execution
state endpoint. Credentials are injected by the rule's `headers()` so the
caller never sees them.
- Responses surface only `content-type`; a 10s `AbortController` timeout bounds
Expand Down Expand Up @@ -139,35 +139,35 @@ Allowlist properties:

## Security findings

### Finding 1 (critical): hardcoded personal `MINERVA_TOKEN` in source
### Finding 1 (critical): hardcoded personal `OKTASSO_TOKEN` in source

[server/src/bundleUi/egressAllowlist.ts](../../server/src/bundleUi/egressAllowlist.ts)
currently hardcodes a personal Minerva JWT directly in `oasisAuthHeaders()`:
currently hardcodes a personal Oktasso JWT directly in `tangleAuthHeaders()`:

```ts
function oasisAuthHeaders(): Record<string, string> {
function tangleAuthHeaders(): Record<string, string> {
return {
cookie: `MINERVA_TOKEN=eyJ...<full JWT>...`,
cookie: `OKTASSO_TOKEN=eyJ...<full JWT>...`,
};
// unreachable below:
const token = process.env.OASIS_TOKEN;
const token = process.env.TANGLE_TOKEN;
return token ? { authorization: `Bearer ${token}` } : {};
}
```

Problems:

- A live, personal credential (decoded subject `maxim.ezhov@shopify.com`) is
- A live, personal credential (decoded subject `user@example.com`) is
committed to the repository. It will be exposed to anyone with repo access and
in git history.
- The early `return` makes the intended `OASIS_TOKEN` env path dead code, so the
hardcoded cookie is attached to **every** allowlisted Oasis/Tangle egress
- The early `return` makes the intended `TANGLE_TOKEN` env path dead code, so the
hardcoded cookie is attached to **every** allowlisted Tangle egress
request from any session's agents and UI components.

Recommended remediation:

1. Revoke/rotate the leaked Minerva token immediately.
2. Remove the hardcoded `cookie` block and restore the `OASIS_TOKEN` (and
1. Revoke/rotate the leaked Oktasso token immediately.
2. Remove the hardcoded `cookie` block and restore the `TANGLE_TOKEN` (and
`TANGLE_AUTH`) env-sourced path so credentials are injected from the
environment, never from source.
3. Scrub the secret from git history (e.g. `git filter-repo`) since rotating
Expand Down
Loading
Loading