diff --git a/a2a/iag-token-exchange/.example.env b/a2a/iag-token-exchange/.example.env index d8a97a6..8c23f19 100644 --- a/a2a/iag-token-exchange/.example.env +++ b/a2a/iag-token-exchange/.example.env @@ -141,11 +141,16 @@ DRIVE_MCP_PORT=8000 DRIVE_MCP_HOST=drive-mcp # [Token Service] Self-hosted issuer of exchanged (delegation) tokens - see -# token-service/README.md. The gateways exchange here instead of at the IdP; -# the delegated token travels in X-IK-Token (policies read it as $ik_token). +# token-service/README.md. The A2A gateways exchange here instead of at the +# IdP (gateway >= 2.47.0 reads the incoming X-IK-Token, so the chain nests +# hop by hop); the delegated token travels in X-IK-Token (policies read it +# as $ik_token). The MCP gateways stay on the IdP exchange until the +# platform mcp-server 2.49.0 rollout (see docker-compose.yaml). # Client credentials must match token-service.yaml idp.client_auth. TOKEN_SERVICE_CLIENT_ID=agent-gateway TOKEN_SERVICE_CLIENT_SECRET= TOKEN_SERVICE_PORT=8102 -# Base URL as the gateways reach it; empty reverts to IdP-side exchange. +# Base URL as the gateways reach it (defaults to http://token-service:8102 +# in docker-compose.yaml; set only to override the address - to disable +# token-service mode, comment the JARVIS_TOKEN_SERVICE_* block out there). # TOKEN_SERVICE_BASE_URL=http://token-service:8102 diff --git a/a2a/iag-token-exchange/README.md b/a2a/iag-token-exchange/README.md index ae27a65..74c3fc4 100644 --- a/a2a/iag-token-exchange/README.md +++ b/a2a/iag-token-exchange/README.md @@ -243,23 +243,26 @@ make new-analyst ```yaml services: iag-base: - image: indykite/agent-gateway:2.43.6 # or any newer tag from Docker Hub + image: indykite/agent-gateway:2.48.0 # or any newer tag from Docker Hub ``` -All gateways inherit this tag. `2.42.x` adds the `token_service` exchange -block (delegation minted by the IndyKite Token Service, travelling in -`X-IK-Token`); `2.21.1` was the first tag with MCP proxying -(`JARVIS_PROTECTED_AGENT_PROTOCOL: mcp`), which the `mcp-iag` and -`drive-mcp-iag` services need - the published `2.0.x` tags ignore the protocol -and 404 every MCP method after the auth pipeline passes. Avoid floating tags -like `latest` so the demo behaviour is reproducible. +All gateways inherit this tag. `2.47.0` makes a gateway in token-service mode +read the incoming `X-IK-Token` (introspected at the Token Service, used as +the subject of this hop's exchange), so multi-hop A2A delegation chains grow +hop by hop - the minimum for the A2A gateways' token-service mode. `2.42.x` +added the `token_service` exchange block itself (delegation minted by the +IndyKite Token Service, travelling in `X-IK-Token`); `2.21.1` was the first +tag with MCP proxying (`JARVIS_PROTECTED_AGENT_PROTOCOL: mcp`), which the +`mcp-iag` and `drive-mcp-iag` services need - the published `2.0.x` tags +ignore the protocol and 404 every MCP method after the auth pipeline passes. +Avoid floating tags like `latest` so the demo behaviour is reproducible. If you are on Apple Silicon, add a `platform` attribute: ```yaml services: iag-base: - image: indykite/agent-gateway:2.43.6 + image: indykite/agent-gateway:2.48.0 platform: linux/amd64 ``` @@ -276,8 +279,12 @@ for different releases. ### Token Service setup -The Token Service issues the exchanged (delegation) tokens for the MCP -gateways. Full build/config/run details live in +The Token Service issues the exchanged (delegation) tokens for the A2A +gateways (the MCP gateways join once the platform's mcp-server `2.49.0` - +which validates the `X-IK-Token` delegated token and accepts any Token +Introspect config of the app space - is rolled out to the target +environment; see the notes in `docker-compose.yaml`). Full +build/config/run details live in [`token-service/README.md`](token-service/README.md); in short: 1. **Build the image** from the jarvis repository diff --git a/a2a/iag-token-exchange/analyst_agent/analyst_agent.py b/a2a/iag-token-exchange/analyst_agent/analyst_agent.py index ecac0bf..24baf7f 100644 --- a/a2a/iag-token-exchange/analyst_agent/analyst_agent.py +++ b/a2a/iag-token-exchange/analyst_agent/analyst_agent.py @@ -1051,16 +1051,20 @@ def _get_ik_token_from_context(context: RequestContext | None) -> str: _background_tasks: set = set() -def _report_exchanged_token(token: str) -> None: +def _report_exchanged_token(token: str, delegation: str = "") -> None: """Post the exchanged bearer token to the console's audit terminal (fire-and-forget). The gateways' audit events carry the decision and actors chain but not the minted delegation token itself, so each agent reports the token it - received; the console renders it as a TOKEN card. Failures never affect - the request. + received; the console renders it as a TOKEN card. In token-service mode + the delegation travels in X-IK-Token beside the user's own bearer, so the + caller passes it too and the card shows the chained token; without one + (classic mode) the Authorization bearer already carries the chain. + Failures never affect the request. """ if not CHATBOT_UPDATES_URL: return + token = delegation or token subject = actor = "?" try: payload = token.split(".")[1] @@ -1672,7 +1676,7 @@ async def execute( # noqa: D102 # surfaced in the console audit terminal to show the exchange chain; # logs carry only a redacted fingerprint to avoid credential leaks. _logger.info("Exchanged bearer token (redacted): %s...%s", access_token[:6], access_token[-6:]) - _report_exchanged_token(access_token) + _report_exchanged_token(access_token, _current_ik_token.get()) await _process_analyst_request(context, event_queue, access_token) diff --git a/a2a/iag-token-exchange/chatbot/static/index.html b/a2a/iag-token-exchange/chatbot/static/index.html index c59df83..0e9cda0 100644 --- a/a2a/iag-token-exchange/chatbot/static/index.html +++ b/a2a/iag-token-exchange/chatbot/static/index.html @@ -361,9 +361,14 @@ actor.textContent = event.actor || '?'; const badge = document.createElement('span'); badge.className = 'audit-badge'; + // Token-service audits carry their own decision vocabulary (RFC 7662 + // introspections, exchange refusals) - label each precisely instead + // of defaulting every unknown decision to NOT AUTHORIZED. badge.textContent = decision === 'authorized' ? 'AUTHORIZED' : decision === 'token_exchanged' ? 'TOKEN' - : 'NOT AUTHORIZED'; + : decision === 'introspected' ? 'INTROSPECTED' + : decision === 'not_authorized' ? 'NOT AUTHORIZED' + : decision.toUpperCase().replaceAll('_', ' '); flow.append(subject, arrow, actor, badge); card.append(head, flow); diff --git a/a2a/iag-token-exchange/chatbot/static/styles.css b/a2a/iag-token-exchange/chatbot/static/styles.css index 6ff830a..0077435 100644 --- a/a2a/iag-token-exchange/chatbot/static/styles.css +++ b/a2a/iag-token-exchange/chatbot/static/styles.css @@ -113,6 +113,13 @@ body { .audit-card.authorized { border-left-color: #4ade80; } .audit-card.not_authorized { border-left-color: #f87171; } .audit-card.token_exchanged { border-left-color: var(--accent); } +/* Informational token-service decisions (successful introspections). */ +.audit-card.introspected { border-left-color: #60a5fa; } +/* Token-service refusals/errors keep the failure red. */ +.audit-card.introspected_inactive, +.audit-card.introspection_refused, +.audit-card.exchange_refused, +.audit-card.error { border-left-color: #f87171; } .audit-card-head { display: flex; @@ -170,6 +177,17 @@ body { color: var(--accent); background: rgba(255, 107, 53, 0.12); } +.audit-card.introspected .audit-badge { + color: #60a5fa; + background: rgba(96, 165, 250, 0.12); +} +.audit-card.introspected_inactive .audit-badge, +.audit-card.introspection_refused .audit-badge, +.audit-card.exchange_refused .audit-badge, +.audit-card.error .audit-badge { + color: #f87171; + background: rgba(248, 113, 113, 0.12); +} .audit-reason { margin-top: 6px; } .audit-reason summary { diff --git a/a2a/iag-token-exchange/docker-compose.yaml b/a2a/iag-token-exchange/docker-compose.yaml index 98dcdbf..b243dc9 100644 --- a/a2a/iag-token-exchange/docker-compose.yaml +++ b/a2a/iag-token-exchange/docker-compose.yaml @@ -51,6 +51,14 @@ services: # Authorization header (policies read it as $ik_token). See token-service/. token-service: image: token-service:local + # The jarvis base image bakes a HEALTHCHECK on :9080/healthz, which only + # the gateway binary serves - the token-service binary does not register + # the healthcheck module, so probe the real endpoint instead. + healthcheck: + test: ["CMD", "/bin/http_health_probe", "http://localhost:8102/.well-known/openid-configuration"] + interval: 10s + timeout: 1s + start_period: 10s ports: - "${TOKEN_SERVICE_PORT:-8102}:8102" networks: @@ -82,6 +90,16 @@ services: JARVIS_PROTECTED_AGENT_BASE_URL: http://${ORCHESTRATOR_HOST}:${ORCHESTRATOR_PORT} JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_ID: ${ORCHESTRATOR_IDP_CLIENT_ID} JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_SECRET: ${ORCHESTRATOR_IDP_CLIENT_SECRET} + # Token Service mode (gateway >= 2.47.0): delegation minted at the + # self-hosted Token Service; the user's token stays in Authorization, + # the chain travels in X-IK-Token and nests hop by hop. Comment the + # block out to fall back to the classic IdP exchange. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} volumes: - ./audit-config.yaml:/app/.configs/audit-config.yaml command: ["--config=/app/.configs/audit-config.yaml"] @@ -138,6 +156,13 @@ services: JARVIS_PROTECTED_AGENT_BASE_URL: http://${RETRIEVER_HOST}:${RETRIEVER_PORT} JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_ID: ${RETRIEVER_IDP_CLIENT_ID} JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_SECRET: ${RETRIEVER_IDP_CLIENT_SECRET} + # Token Service mode (gateway >= 2.47.0) - see orchestrator-iag. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} volumes: - ./audit-config.yaml:/app/.configs/audit-config.yaml command: ["--config=/app/.configs/audit-config.yaml"] @@ -194,6 +219,13 @@ services: # The weather agent runs wf2 (weather -> mcp), not the base ${WORKFLOW_ID} # (wf1). allowed_workflow_id is a single value per gateway. JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID: ${WEATHER_WORKFLOW_ID} + # Token Service mode (gateway >= 2.47.0) - see orchestrator-iag. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} volumes: - ./audit-config.yaml:/app/.configs/audit-config.yaml command: ["--config=/app/.configs/audit-config.yaml"] @@ -217,6 +249,13 @@ services: # The analyst runs its own workflow (wf3: millicent -> analyst -> mcp), not the # base ${WORKFLOW_ID} (wf1). allowed_workflow_id is a single value per gateway. JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID: ${ANALYST_WORKFLOW_ID} + # Token Service mode (gateway >= 2.47.0) - see orchestrator-iag. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} volumes: - ./audit-config.yaml:/app/.configs/audit-config.yaml command: ["--config=/app/.configs/audit-config.yaml"] @@ -276,12 +315,20 @@ services: - mcp-iag-network environment: K_SERVICE: mcp-iag - # Token Service mode is OFF for now: the platform's MCP endpoint gates - # Authorization on a single bound issuer+audience shape, which requires - # the classic final IdP exchange (aud = this gateway's client). Set the - # JARVIS_TOKEN_SERVICE_* block here once the platform supports the - # per-path audiences (see token-service/README.md and the findings - # report). + # Token Service mode is ON (verified 2026-08-28: RC runs mcp-server + # 2.49.0 - a garbage X-IK-Token gets 400 invalid_request naming the + # header). The platform no longer gates Authorization on the single + # bound issuer+audience shape (any Token Introspect config of the app + # space authenticates) and validates the X-IK-Token delegation itself. + # This block is REQUIRED while the A2A gateways run token-service + # mode: their chain travels in X-IK-Token, which a classic-mode + # gateway ignores - mixed modes on one path break the chain. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} JARVIS_SERVICE_NAME: mcp-iag JARVIS_SERVICE_PORT: ${IAG_MCP_PORT} # Switch the gateway out of the default "a2a" proxy mode into MCP proxy mode. @@ -349,12 +396,13 @@ services: - drive-mcp-iag-network environment: K_SERVICE: drive-mcp-iag - # Token Service mode is OFF for now: the platform's MCP endpoint gates - # Authorization on a single bound issuer+audience shape, which requires - # the classic final IdP exchange (aud = this gateway's client). Set the - # JARVIS_TOKEN_SERVICE_* block here once the platform supports the - # per-path audiences (see token-service/README.md and the findings - # report). + # Token Service mode is ON - same block and reasoning as mcp-iag. + JARVIS_TOKEN_SERVICE_BASE_URL: ${TOKEN_SERVICE_BASE_URL:-http://token-service:8102} + JARVIS_TOKEN_SERVICE_EXCHANGE_ENDPOINT: "oauth2/token" + JARVIS_TOKEN_SERVICE_INTROSPECT_ENDPOINT: "oauth2/introspect" + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_TYPE: client_secret_basic + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_ID: ${TOKEN_SERVICE_CLIENT_ID:-agent-gateway} + JARVIS_TOKEN_SERVICE_CLIENT_AUTH_CLIENT_SECRET: ${TOKEN_SERVICE_CLIENT_SECRET} JARVIS_SERVICE_NAME: drive-mcp-iag JARVIS_SERVICE_PORT: ${IAG_DRIVE_MCP_PORT:-8887} # MCP proxy mode, same as mcp-iag. diff --git a/a2a/iag-token-exchange/iag-base-docker.yaml b/a2a/iag-token-exchange/iag-base-docker.yaml index 88ef85c..25bf0a0 100644 --- a/a2a/iag-token-exchange/iag-base-docker.yaml +++ b/a2a/iag-token-exchange/iag-base-docker.yaml @@ -3,12 +3,20 @@ version: '3.9' services: iag-base: - # Pinned: 2.42.x adds the token_service block (delegation minted by the - # IndyKite Token Service; delegated token travels in X-IK-Token, policies - # read it as $ik_token). 2.21.1 was the previous pin (first with MCP - # proxying). All gateways share this tag. + # Pinned: 2.47.0+ reads the incoming X-IK-Token in token-service mode + # (introspected at the Token Service, subject of this hop's exchange), so + # multi-hop A2A delegation chains grow hop by hop. 2.42.x added the + # token_service block (delegation minted by the IndyKite Token Service; + # travels in X-IK-Token, policies read it as $ik_token); 2.21.1 was the + # first tag with MCP proxying. All gateways share this tag. # See https://hub.docker.com/r/indykite/agent-gateway/tags. - image: indykite/agent-gateway:2.43.6 + image: indykite/agent-gateway:2.48.0 + # The image bakes a HEALTHCHECK on :9080/healthz, but the 2.48.0 gateway + # binary registers no healthcheck server and every service-port route + # needs auth (the probe insists on a 200), so every gateway would sit + # "unhealthy" forever. Disable it - a plain "Up" is the honest status. + healthcheck: + disable: true environment: JARVIS_SERVICE_LOG_LEVEL: debug JARVIS_SERVICE_ENVIRONMENT: demo @@ -17,13 +25,15 @@ services: JARVIS_IDENTITY_PROVIDER_CLIENT_CREDENTIAL_ENDPOINT: "oauth-token" JARVIS_IDENTITY_PROVIDER_EXCHANGE_ENDPOINT: "oauth-token" # NOTE on the Token Service (JARVIS_TOKEN_SERVICE_*): it is enabled per - # gateway in docker-compose.yaml, NOT here. On 2.42.0 a gateway in - # token-service mode keeps the user's raw token in Authorization and - # ignores incoming X-IK-Token when building the actors chain, so - # multi-hop A2A chains cannot grow. The A2A gateways therefore keep the - # classic IdP exchange (chain travels in Authorization), and only the - # MCP gateways - the last hop before the platform - mint their - # delegation at the Token Service. + # gateway in docker-compose.yaml, NOT here. Since 2.47.0 a gateway in + # token-service mode reads the incoming X-IK-Token (introspects it at + # the Token Service, requires its sub to match the access token) and + # exchanges IT, so the act chain nests hop by hop - the A2A gateways + # now mint their delegation at the Token Service (user token stays in + # Authorization, chain travels in X-IK-Token). The MCP gateways keep + # the classic IdP exchange until the platform MCP server that validates + # delegated tokens (mcp-server 2.49.0) is rolled out to the target + # environment - see the notes on mcp-iag / drive-mcp-iag. JARVIS_CONTX_IQ_BASE_URL: ${INDYKITE_BASE_URL}/contx-iq/v1 JARVIS_CONTX_IQ_QUERY_ID: ${CIQ_QUERY_ID} JARVIS_CONTX_IQ_APP_AGENT_CREDENTIALS_TOKEN: ${APP_AGENT_CREDENTIALS_TOKEN} diff --git a/a2a/iag-token-exchange/orchestrator_agent/orchestrator_agent.py b/a2a/iag-token-exchange/orchestrator_agent/orchestrator_agent.py index 40ad5c7..03db9f5 100644 --- a/a2a/iag-token-exchange/orchestrator_agent/orchestrator_agent.py +++ b/a2a/iag-token-exchange/orchestrator_agent/orchestrator_agent.py @@ -184,16 +184,20 @@ def _get_ik_token_from_context(context: "RequestContext | None") -> str: _background_tasks: set = set() -def _report_exchanged_token(token: str) -> None: +def _report_exchanged_token(token: str, delegation: str = "") -> None: """Post the exchanged bearer token to the console's audit terminal (fire-and-forget). The gateways' audit events carry the decision and actors chain but not the minted delegation token itself, so each agent reports the token it - received; the console renders it as a TOKEN card. Failures never affect - the request. + received; the console renders it as a TOKEN card. In token-service mode + the delegation travels in X-IK-Token beside the user's own bearer, so the + caller passes it too and the card shows the chained token; without one + (classic mode) the Authorization bearer already carries the chain. + Failures never affect the request. """ if not CHATBOT_UPDATES_URL: return + token = delegation or token subject = actor = "?" try: payload = token.split(".")[1] @@ -824,7 +828,7 @@ async def execute( # noqa: C901,D102,PLR0912 # skipcq: PY-R1000,PYL-R0201 # surfaced in the console audit terminal to show the exchange chain; # logs carry only a redacted fingerprint to avoid credential leaks. _logger.info("Exchanged bearer token (redacted): %s...%s", access_token[:6], access_token[-6:]) - _report_exchanged_token(access_token) + _report_exchanged_token(access_token, _current_ik_token.get()) # SDK 1.0: context.message.parts is list[Part]; Part.text is the text field directly. raw_text = "" diff --git a/a2a/iag-token-exchange/retriever_agent/retriever_agent.py b/a2a/iag-token-exchange/retriever_agent/retriever_agent.py index 5cb6a0f..186485a 100644 --- a/a2a/iag-token-exchange/retriever_agent/retriever_agent.py +++ b/a2a/iag-token-exchange/retriever_agent/retriever_agent.py @@ -971,16 +971,20 @@ def _get_ik_token_from_context(context: RequestContext | None) -> str: _background_tasks: set = set() -def _report_exchanged_token(token: str) -> None: +def _report_exchanged_token(token: str, delegation: str = "") -> None: """Post the exchanged bearer token to the console's audit terminal (fire-and-forget). The gateways' audit events carry the decision and actors chain but not the minted delegation token itself, so each agent reports the token it - received; the console renders it as a TOKEN card. Failures never affect - the request. + received; the console renders it as a TOKEN card. In token-service mode + the delegation travels in X-IK-Token beside the user's own bearer, so the + caller passes it too and the card shows the chained token; without one + (classic mode) the Authorization bearer already carries the chain. + Failures never affect the request. """ if not CHATBOT_UPDATES_URL: return + token = delegation or token subject = actor = "?" try: payload = token.split(".")[1] @@ -1518,7 +1522,7 @@ async def execute( # noqa: D102 # surfaced in the console audit terminal to show the exchange chain; # logs carry only a redacted fingerprint to avoid credential leaks. _logger.info("Exchanged bearer token (redacted): %s...%s", access_token[:6], access_token[-6:]) - _report_exchanged_token(access_token) + _report_exchanged_token(access_token, _current_ik_token.get()) await _process_retriever_request(context, event_queue, access_token) diff --git a/a2a/iag-token-exchange/token-service/README.md b/a2a/iag-token-exchange/token-service/README.md index fb7cd4d..b1c3ac3 100644 --- a/a2a/iag-token-exchange/token-service/README.md +++ b/a2a/iag-token-exchange/token-service/README.md @@ -64,14 +64,25 @@ configuration (issuer + audience) and verify against its keys. `token-service` service in [`../docker-compose.yaml`](../docker-compose.yaml) joins every gateway network. 2. ~~Bump the gateway image and point its exchange block at this service~~ - - done: gateways run `2.42.x` (which added the `JARVIS_TOKEN_SERVICE_*` - block), and idsvr-issued subject tokens are accepted via a second - introspection configuration (`jwks_uri` of the IdP). -3. **Platform acceptance - pending**: the project's Token Introspect config - trusts this issuer (inline public JWKS, `https` issuer required), but the - platform MCP gate still rejects Token-Service-issued JWTs, so the - `JARVIS_TOKEN_SERVICE_*` blocks stay commented out in compose and the MCP - gateways keep the classic IdP exchange until that is fixed platform-side. + done: gateways run `2.48.0`. `2.42.x` added the `JARVIS_TOKEN_SERVICE_*` + block; `2.47.0` made the gateway read the incoming `X-IK-Token` + (introspected here, subject of the next exchange), so the **A2A gateways + now run token-service mode with multi-hop chains** - the block is live on + orchestrator/retriever/weather/analyst in `docker-compose.yaml`. + idsvr-issued subject tokens are accepted via a second introspection + configuration (`jwks_uri` of the IdP). +3. **Platform acceptance - fixed in mcp-server `2.49.0` (2026-08-28), + rollout pending**: the MCP endpoint no longer gates Authorization on the + single issuer+audience shape the MCP config binds (any Token Introspect + config of the app space authenticates) and now introspects the + `X-IK-Token` delegated token itself (`400 invalid_request` on a bad one, + `sub` must match the access token). The project's Token Introspect config + already trusts this issuer (inline public JWKS, `https` issuer required). + The MCP gateways keep the classic IdP exchange until the release reaches + the target environment - probe: a valid Bearer plus a garbage + `X-IK-Token` answers `400` naming the header on the new server, while the + old one ignores it - then copy the `JARVIS_TOKEN_SERVICE_*` block onto + `mcp-iag`/`drive-mcp-iag`. 4. ~~Point the audit section at the demo's audit terminal~~ - done: every exchange attempt is delivered to the chatbot's audit webhook (`audit.http.url`), tagged `service: token-service`. diff --git a/a2a/iag-token-exchange/token-service/token-service.example.yaml b/a2a/iag-token-exchange/token-service/token-service.example.yaml index 4b14425..0574ce7 100644 --- a/a2a/iag-token-exchange/token-service/token-service.example.yaml +++ b/a2a/iag-token-exchange/token-service/token-service.example.yaml @@ -39,9 +39,12 @@ idp: # Which incoming subject/actor tokens are accepted, and how they are verified. # Phase 1: tokens signed by the local demo IdP key (see mint.py in the session # scratchpad). Phase 2 adds the real IdP (idsvr) issuer with its jwks_uri. +# NOTE: since jarvis c722ed066, `configurations` is a MAP keyed by the +# configuration name (a `- name: x` list entry fails startup with +# "'configurations[0][name]' expected a map or struct, got \"string\""). token_introspection: configurations: - - name: demo-idp + demo-idp: token_types: - subject_token - actor_token diff --git a/a2a/iag-token-exchange/weather_agent/weather_agent.py b/a2a/iag-token-exchange/weather_agent/weather_agent.py index 4488d02..3475ade 100644 --- a/a2a/iag-token-exchange/weather_agent/weather_agent.py +++ b/a2a/iag-token-exchange/weather_agent/weather_agent.py @@ -249,16 +249,20 @@ def _get_ik_token_from_context(context: "RequestContext | None") -> str: _background_tasks: set = set() -def _report_exchanged_token(token: str) -> None: +def _report_exchanged_token(token: str, delegation: str = "") -> None: """Post the exchanged bearer token to the console's audit terminal (fire-and-forget). The gateways' audit events carry the decision and actors chain but not the minted delegation token itself, so each agent reports the token it - received; the console renders it as a TOKEN card. Failures never affect - the request. + received; the console renders it as a TOKEN card. In token-service mode + the delegation travels in X-IK-Token beside the user's own bearer, so the + caller passes it too and the card shows the chained token; without one + (classic mode) the Authorization bearer already carries the chain. + Failures never affect the request. """ if not CHATBOT_UPDATES_URL: return + token = delegation or token subject = actor = "?" try: payload = token.split(".")[1] @@ -796,7 +800,7 @@ async def execute( # noqa: D102 # skipcq: PYL-R0201 # surfaced in the console audit terminal to show the exchange chain; # logs carry only a redacted fingerprint to avoid credential leaks. _logger.info("Exchanged bearer token (redacted): %s...%s", access_token[:6], access_token[-6:]) - _report_exchanged_token(access_token) + _report_exchanged_token(access_token, _current_ik_token.get()) prompt = _message_text(context) _logger.info("Received message for %s: %s", WEATHER_AGENT_NAME, prompt) diff --git a/instant-stack/.env.example b/instant-stack/.env.example index 0e6ce36..e8872c2 100644 --- a/instant-stack/.env.example +++ b/instant-stack/.env.example @@ -17,3 +17,13 @@ DATASET=canbank # (e.g. get-self, get-stock-quote, get-stock-trade-threshold, get-internal-documents, get-decisions). # not necessary if you don't test ciq execute in this app USER_TOKEN= + +# [canbank-ts dataset only] Public signing JWK of the self-hosted token-service +# (iag-token-exchange), substituted into the token-service-https-chatbot-mcp +# token-introspect config via the ${TOKEN_SERVICE_PUBLIC_JWK} manifest +# placeholder. One JWK as a single-line JSON string, stripped to +# kty/n/e/alg/kid/use (the config API rejects the published key_ops). Fetch: +# curl -s http://localhost:8102/.well-known/jwks.json | python3 -c " +# import sys, json; k = json.load(sys.stdin)['keys'][0] +# print(json.dumps({f: k[f] for f in ('kty','n','e','alg','kid','use')}))" +TOKEN_SERVICE_PUBLIC_JWK= diff --git a/instant-stack/api/_dataset.py b/instant-stack/api/_dataset.py index 5c81d8e..e330fee 100644 --- a/instant-stack/api/_dataset.py +++ b/instant-stack/api/_dataset.py @@ -20,6 +20,7 @@ import json import os +import re from pathlib import Path from dotenv import load_dotenv @@ -134,7 +135,52 @@ def resolver_for_slot(slot: str) -> dict: # project_id is supplied at runtime from env. offline_validation is kept (not # online_validation) so IdP ID tokens validate against the issuer JWKS instead # of /userinfo. -TOKEN_INTROSPECT: dict = _MANIFEST.get("token_introspect", {}) +# The manifest holds a list of configs; a bare object (the pre-list manifest +# format) is accepted and treated as a one-element list. Keep the config the +# MCP server should bind to FIRST: its ID is recorded as TOKEN_INTROSPECT_ID, +# which _mcp_server_payload reads. +_TI_RAW = _MANIFEST.get("token_introspect", {}) +TOKEN_INTROSPECTS: list = _TI_RAW if isinstance(_TI_RAW, list) else ([_TI_RAW] if _TI_RAW else []) +# First config kept under the old name for callers that predate the list. +TOKEN_INTROSPECT: dict = TOKEN_INTROSPECTS[0] if TOKEN_INTROSPECTS else {} + +# ${ENV_VAR} placeholders in token-introspect manifest strings, e.g. the +# token-service public JWK in offline_validation.public_jwks - secrets and +# per-deployment values stay in .env instead of being committed in the +# manifest. +_ENV_PLACEHOLDER = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") + + +def _resolve_env_placeholders(value): + """Recursively expand ``${ENV_VAR}`` placeholders from the environment. + + An unset variable resolves to the empty string (and is logged by the + caller's request failing visibly at the Config API, not silently here). + """ + if isinstance(value, str): + return _ENV_PLACEHOLDER.sub(lambda m: os.getenv(m.group(1), ""), value) + if isinstance(value, list): + return [_resolve_env_placeholders(v) for v in value] + if isinstance(value, dict): + return {k: _resolve_env_placeholders(v) for k, v in value.items()} + return value + + +def token_introspect_env_key(index: int) -> str: + """Env key the created config's ID is recorded under. + + The first config keeps the historical TOKEN_INTROSPECT_ID name so existing + .env files (and the MCP server binding) keep their meaning; later ones get + TOKEN_INTROSPECT_ID_. + """ + return "TOKEN_INTROSPECT_ID" if index == 0 else f"TOKEN_INTROSPECT_ID_{index + 1}" + + +def token_introspect_at(index: int) -> dict: + """Return the token-introspect config at *index*, with env placeholders resolved.""" + cfg = TOKEN_INTROSPECTS[index] if index < len(TOKEN_INTROSPECTS) else {} + return _resolve_env_placeholders(cfg) + # --- KBAC authorization policies (migrated from api/authorization_policy.py) --- # Static KBAC form defaults. Both api/authorization_policy.py and provision.py's diff --git a/instant-stack/api/project.py b/instant-stack/api/project.py index c825fd2..0ef64a4 100644 --- a/instant-stack/api/project.py +++ b/instant-stack/api/project.py @@ -64,9 +64,10 @@ def update_env_variable(key, value): def clean_env_file(): """Remove the project-owned environment variables from the .env file. - Keeps SA_TOKEN, URL_ENDPOINTS, ORGANIZATION_ID, and DATASET - settings not - owned by the project lifecycle - so deleting a project doesn't silently - switch the app back to the default dataset. + Keeps SA_TOKEN, URL_ENDPOINTS, ORGANIZATION_ID, DATASET and + TOKEN_SERVICE_PUBLIC_JWK - settings not owned by the project lifecycle - + so deleting a project doesn't silently switch the app back to the default + dataset or drop the token-service key the canbank-ts manifest resolves. """ env_file = Path(__file__).parent.parent / ".env" @@ -81,7 +82,10 @@ def clean_env_file(): # Settings not owned by the project lifecycle survive the cleanup. DATASET # selects which data// bundle is active - dropping it would flip the # app back to the default dataset after every project deletion. - keep_vars = ["SA_TOKEN", "URL_ENDPOINTS", "ORGANIZATION_ID", "DATASET"] + # TOKEN_SERVICE_PUBLIC_JWK belongs to the local token-service deployment, + # not to any project - the canbank-ts manifest resolves it via a + # ${TOKEN_SERVICE_PUBLIC_JWK} placeholder on the next provisioning run. + keep_vars = ["SA_TOKEN", "URL_ENDPOINTS", "ORGANIZATION_ID", "DATASET", "TOKEN_SERVICE_PUBLIC_JWK"] updated_lines = [] removed_keys = [] @@ -111,7 +115,9 @@ def clean_env_file(): for var in removed_keys: os.environ.pop(var, None) - logger.info("Cleaned .env file, keeping SA_TOKEN, URL_ENDPOINTS, ORGANIZATION_ID, and DATASET") + logger.info( + "Cleaned .env file, keeping SA_TOKEN, URL_ENDPOINTS, ORGANIZATION_ID, DATASET, and TOKEN_SERVICE_PUBLIC_JWK", + ) class Unauthorized(BaseModel): diff --git a/instant-stack/api/provision.py b/instant-stack/api/provision.py index b577642..b399aab 100644 --- a/instant-stack/api/provision.py +++ b/instant-stack/api/provision.py @@ -203,11 +203,14 @@ def _mcp_server_payload(): } -def _token_introspect_payload(): +def _token_introspect_payload(index: int = 0): # Mirrors the /api_token_introspect/create form: JSON-valued fields are # posted as JSON strings, perform_upsert as a "true"/"false" string. Values - # come from the dataset manifest (token_introspect section). - ti = _dataset.TOKEN_INTROSPECT + # come from the dataset manifest (token_introspect section - a list; env + # placeholders like ${TOKEN_SERVICE_PUBLIC_JWK} are resolved at read time). + # env_key tells the create route where to record the resulting ID, so + # several configs don't overwrite each other's entry. + ti = _dataset.token_introspect_at(index) return { "name": ti.get("name", ""), "display_name": ti.get("display_name", ""), @@ -218,6 +221,7 @@ def _token_introspect_payload(): "offline_validation": json.dumps(ti.get("offline_validation", {})), "perform_upsert": "true" if ti.get("perform_upsert", True) else "false", "project_id": os.getenv("PROJECT_ID", ""), + "env_key": _dataset.token_introspect_env_key(index), } @@ -270,15 +274,24 @@ def _label(cfg, prefix): ["APP_AGENT_ID", "APP_TOKEN"], ), ) - if _dataset.TOKEN_INTROSPECT: - steps.append( - _step( - _label(_dataset.TOKEN_INTROSPECT, "Create token introspect"), - "/api_token_introspect/create", - _token_introspect_payload, - ["TOKEN_INTROSPECT_ID"], + # One step per token-introspect config in the manifest (a list; a bare + # object is treated as one entry). The first config's ID keeps the + # TOKEN_INTROSPECT_ID name - the MCP server step binds to it - and later + # ones record TOKEN_INTROSPECT_ID_ (see _dataset.token_introspect_env_key). + steps.extend( + _step( + _label( + ti, + f"Create token introspect {i + 1}" + if len(_dataset.TOKEN_INTROSPECTS) > 1 + else "Create token introspect", ), + "/api_token_introspect/create", + lambda i=i: _token_introspect_payload(i), + [_dataset.token_introspect_env_key(i)], ) + for i, ti in enumerate(_dataset.TOKEN_INTROSPECTS) + ) if _dataset.MCP_SERVER: steps.append( _step( diff --git a/instant-stack/api/token_introspect.py b/instant-stack/api/token_introspect.py index 2310371..584c0e9 100644 --- a/instant-stack/api/token_introspect.py +++ b/instant-stack/api/token_introspect.py @@ -88,7 +88,16 @@ def show_create_form(): # online_validation: it verifies the token signature against the issuer's # JWKS; online_validation would call the IdP's /userinfo, which rejects ID # tokens (401 "Invalid token in Authorization header"). - ti = _dataset.TOKEN_INTROSPECT + # The manifest holds a list of configs; ?index= (0-based) pre-fills the + # nth one, default the first. ${ENV_VAR} placeholders are resolved from + # the environment (e.g. the token-service public JWK). + try: + index = max(0, int(request.args.get("index", "0"))) + except ValueError: + index = 0 + if _dataset.TOKEN_INTROSPECTS: + index = min(index, len(_dataset.TOKEN_INTROSPECTS) - 1) + ti = _dataset.token_introspect_at(index) default_data = { "claims_mapping": ti.get("claims_mapping", {}), "description": ti.get("description", ""), @@ -99,8 +108,23 @@ def show_create_form(): "offline_validation": ti.get("offline_validation", {}), "perform_upsert": ti.get("perform_upsert", True), "project_id": project_id, + # Which .env entry records the created ID - travels through the form + # as a hidden field so manual submits don't all overwrite + # TOKEN_INTROSPECT_ID (the entry the MCP server binding reads). + "env_key": _dataset.token_introspect_env_key(index), } - return render_template("token_introspect/create_form.html", default_data=default_data) + # Selector data: one tab per manifest config, so the single landing-page + # button reaches every config of a multi-entry dataset (e.g. canbank-ts). + configs = [ + {"name": cfg.get("name", f"config {i + 1}"), "env_key": _dataset.token_introspect_env_key(i)} + for i, cfg in enumerate(_dataset.TOKEN_INTROSPECTS) + ] + return render_template( + "token_introspect/create_form.html", + default_data=default_data, + configs=configs, + index=index, + ) @api_token_introspect.post("/create", tags=[tag]) @@ -165,6 +189,14 @@ def create_token_introspect(): "response_text": response.text[:500] if response.text else "No response body", } + # Which .env entry records this config's ID. Sent by provisioning so the + # 2nd+ manifest config doesn't overwrite the 1st (see + # _dataset.token_introspect_env_key); restricted to the + # TOKEN_INTROSPECT_ID* family so the form can't write arbitrary .env keys. + env_key = request.form.get("env_key", "TOKEN_INTROSPECT_ID") + if not re.fullmatch(r"TOKEN_INTROSPECT_ID(_\d+)?", env_key): + env_key = "TOKEN_INTROSPECT_ID" + # Extract and save token introspect ID if the request was successful token_introspect_id_saved = False token_introspect_id = None @@ -176,11 +208,11 @@ def create_token_introspect(): if token_introspect_id: try: - update_env_variable("TOKEN_INTROSPECT_ID", token_introspect_id) + update_env_variable(env_key, token_introspect_id) token_introspect_id_saved = True - logger.info("Saved TOKEN_INTROSPECT_ID: %s", token_introspect_id) + logger.info("Saved %s: %s", env_key, token_introspect_id) except Exception: - logger.exception("Failed to save TOKEN_INTROSPECT_ID") + logger.exception("Failed to save %s", env_key) return render_template( "token_introspect/result.html", diff --git a/instant-stack/data/canbank-ts/manifest.json b/instant-stack/data/canbank-ts/manifest.json new file mode 100644 index 0000000..0173259 --- /dev/null +++ b/instant-stack/data/canbank-ts/manifest.json @@ -0,0 +1,838 @@ +{ + "dataset": "canbank-ts", + "display_name": "CanBank IAG demo (Token Service)", + "description": "Config-as-data for the canbank-iag provisioning app (scaffold: CIQ knowledge queries). Token-service-mode variant: token_introspect additionally covers raw user tokens (aud chatbot-mcp) and token-service delegations arriving in X-IK-Token.", + "ciq_queries": [ + { + "slot": "1", + "name": "get-self", + "display_name": "Get Self", + "description": "Retrieve the authenticated user's profile, department and manager. Call tool 'ciq_execute' with no input_params. Example: { \"id\": \"get-self\", \"input_params\": { } }.", + "query": { + "nodes": [ + "subject.external_id", + "subject.property.name", + "subject.property.email", + "subject.property.title", + "department.property.name", + "manager.property.name" + ], + "relationships": [], + "aggregate_values": [ + "agent" + ] + } + }, + { + "slot": "2", + "name": "get-stock-quote", + "display_name": "Get Stock Quote", + "description": "Get Stock Quote retrieves the user, their department and a stock quote for the requested ticker symbol.The only information required is the currently authenticated user's bearer token and a ticker symbol for the stock. Use this function when the user asks questions about stock quotes, such as: 'What is the current price of ?' Returns: An object containing the user's details, including full_name, department, and the stock price. Call tool 'ciq_execute' with the user's bearer token as the authorization header. Use the following arguments: \"id\": \"get-stock-quote\" and \"ticker\". Example: { \"id\": \"get-stock-quote\", \"input_params\": { \"ticker\": \"NVDA\"} }.\"", + "query": { + "nodes": [ + "subject.external_id", + "subject.property.name", + "subject.property.title", + "department.property.name", + "quote.property.price" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "3", + "name": "get-stock-trade-threshold", + "display_name": "Get Stock Trade Threshold", + "description": "Retrieve a customer's stock account tier and threshold. Caller must be in the trading department. Example: { \"id\": \"get-stock-trade-threshold\", \"input_params\": { \"customer_external_id\": \"ted\" } }.", + "query": { + "nodes": [ + "subject.external_id", + "subject.property.name", + "subject.property.title", + "customer.property.name", + "tier.property.name", + "tier.property.threshold_amount" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "4", + "name": "get-internal-documents", + "display_name": "Get Internal Documents", + "description": "Retrieve internal documents visible to the caller filtered by taxonomy. Example: { \"id\": \"get-internal-documents\", \"input_params\": { \"taxonomy_external_id\": \"policy\" } }.", + "query": { + "nodes": [ + "subject.external_id", + "subject.property.name", + "subject.property.title", + "document.external_id", + "document.property.name", + "document.property.description", + "document.property.url", + "taxonomy.property.name" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "5", + "name": "get-customer-facing-documents", + "display_name": "Get Customer Facing Documents", + "description": "Retrieve customer facing documents with their regulatory agreements and investment categories. Example: { \"id\": \"get-customer-facing-documents\", \"input_params\": { } }.", + "query": { + "nodes": [ + "doc.external_id", + "doc.property.name", + "ra.property.name", + "ic.property.name" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "6", + "name": "get-regulatory-agreements", + "display_name": "Get Regulatory Agreements", + "description": "Retrieve the full list of regulatory agreements. Example: { \"id\": \"get-regulatory-agreements\", \"input_params\": { } }.", + "query": { + "nodes": [ + "ra.external_id", + "ra.property.name", + "ra.property.description" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "7", + "name": "get-decisions", + "display_name": "Get Decisions", + "description": "get-decisions retrieves past decisions that have been made based on a document, probably a policy of some kind. The results include: the actor, the decision, the document applied in the decision, a relationship document_applied.type that describes the relationship between the decision and the document, the support ticket, and account. The only information required is the currently authenticated user's bearer token. Use this function when we need to find past decisions such as: \"What decisions incorporated the refund policy document?\" Returns: An object containing the decision, the document, a relationship that explains how the document was applied, the actor, the support ticket and the affected account. Call tool 'ciq_execute' with the user's bearer token as the authorization header. Use the following arguments: \"id\": \"get-decisions\". Example: { \"id\": \"get-decisions\", \"input_params\": { \"document_external_id\": \"refund_policy\" } }.", + "query": { + "nodes": [ + "actor.property.name", + "actor.property.title", + "document.property.name", + "document.property.description", + "document.property.url", + "decision.external_id", + "decision.property.description", + "decision.create_time", + "account.property.account_type", + "ticket.external_id", + "ticket.property.description" + ], + "relationships": [ + "document_applied.type" + ], + "aggregate_values": [] + } + }, + { + "slot": "8", + "name": "get-agent-workflows", + "display_name": "Get Agent Workflows", + "description": "\n Given an agent id return the list of workflows within which that agent participates.\n Example: { \"id\": \"get-agent-workflows\", \"input_params\": { \"agent_id\": \"agent_id\" } }\n ", + "query": { + "nodes": [], + "relationships": [], + "aggregate_values": [ + "workflow", + "agent_list" + ] + } + }, + { + "slot": "9", + "name": "get-hq-weather", + "display_name": "Get HQ Weather", + "description": "Get HQ Weather returns the current weather reading for CanBank's London headquarters: temperature, apparent temperature, wind speed and weather code, together with the unit labels. The hq_weather Weather node carries `latitude` and `longitude` properties that are substituted into the `weather` external data resolver URL. The `current` and `units` properties are populated live from open-meteo at query time. Call tool 'ciq_execute' with no input_params. Example: { \"id\": \"get-hq-weather\", \"input_params\": { } }.", + "query": { + "nodes": [ + "weather.external_id", + "weather.property.location", + "weather.property.latitude", + "weather.property.longitude", + "weather.property.current", + "weather.property.units" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "10", + "name": "store-decision", + "display_name": "Store Decision", + "description": "Store Decision records a new decision in the knowledge graph: it creates a Decision node and links it to the policy document it applies (APPLIES), the support ticket it closes (CLOSED_BY) and the authenticated user who made it (MADE), all in one atomic operation. Use this function when a decision has been reached and must be persisted, such as: 'Record that I approved this refund as an exception to the refund policy.' Requires the user's bearer token. Call tool 'ciq_execute' with the following arguments: \"id\": \"store-decision\" and input_params decision_external_id, decision_description, document_external_id, ticket_external_id. Example: { \"id\": \"store-decision\", \"input_params\": { \"decision_external_id\": \"decision_002\", \"decision_description\": \"Refund approved as an exception\", \"document_external_id\": \"refund_policy\", \"ticket_external_id\": \"ticket_001\" } }.", + "query": { + "upsert_nodes": [ + { + "name": "decision", + "type": "Decision", + "external_id": "$decision_external_id", + "properties": [ + { + "type": "description", + "value": "$decision_description" + } + ] + } + ], + "upsert_relationships": [ + { + "name": "r_applies", + "source": "decision", + "target": "document", + "type": "APPLIES" + }, + { + "name": "r_made", + "source": "subject", + "target": "decision", + "type": "MADE" + }, + { + "name": "r_closed", + "source": "ticket", + "target": "decision", + "type": "CLOSED_BY" + } + ], + "nodes": [ + "decision.external_id", + "decision.property.description" + ], + "relationships": [ + "r_applies", + "r_made", + "r_closed" + ] + } + }, + { + "slot": "11", + "name": "explain-staff-workflow-access", + "display_name": "Explain Staff Workflow Access", + "description": "Authorization-path explainer for the console's why? cards: returns the staff path User -WORKS_IN-> Department -CAN_TRIGGER-> Workflow when it exists in the graph (empty data = no such path). Example: { \"id\": \"explain-staff-workflow-access\", \"input_params\": { \"subject_id\": \"millicent\", \"workflow_id\": \"wf1\" } }.", + "query": { + "nodes": [ + "u.external_id", + "u.property.name", + "u.property.first_name", + "d.external_id", + "d.property.name", + "wf.external_id" + ], + "relationships": [], + "aggregate_values": [] + } + }, + { + "slot": "12", + "name": "explain-direct-workflow-access", + "display_name": "Explain Direct Workflow Access", + "description": "Authorization-path explainer for the console's why? cards: returns the direct User -CAN_TRIGGER-> Workflow edge when it exists in the graph (empty data = no such path). Example: { \"id\": \"explain-direct-workflow-access\", \"input_params\": { \"subject_id\": \"james\", \"workflow_id\": \"wf1\" } }.", + "query": { + "nodes": [ + "u.external_id", + "u.property.name", + "u.property.first_name", + "wf.external_id" + ], + "relationships": [], + "aggregate_values": [] + } + } + ], + "ciq_policies": [ + { + "slot": "1", + "name": "get-self", + "display_name": "Get Self", + "description": "Return the authenticated user, their department and their manager.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub}) WITH subject OPTIONAL MATCH (subject)-[:WORKS_IN]->(department:Department) WITH subject, department OPTIONAL MATCH (subject)-[:REPORTS_TO]->(manager:User) WITH subject, department, manager, $token.act.sub AS agent", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "department.*", + "manager.*" + ], + "relationships": [], + "aggregate_values": [ + "agent" + ] + } + }, + "tags": [ + "canbank", + "self", + "profile" + ] + }, + { + "slot": "2", + "name": "get-stock-quote", + "display_name": "Get Stock Quote", + "description": "Return a stock quote when the caller's department is allowed to retrieve it.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub})-[:WORKS_IN]->(department:Department)-[:CAN_RETRIEVE]->(quote:Quote)", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "department.*", + "quote.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "trading", + "quote" + ] + }, + { + "slot": "3", + "name": "get-stock-trade-threshold", + "display_name": "Get Stock Trade Threshold", + "description": "Return a customer's stock account tier when accessed by a trading user.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub})-[:WORKS_IN]->(department:Department)-[:CAN_ACCESS|CONTAINS*..2]->(customer:Customer {external_id: $customer_external_id})-[:OWNS]-(account:Account)-[:IS_TIER]->(tier:AccountTier) WHERE (account)-[:IS_TYPE]->(:InvestmentCategory {external_id: 'cat_stocks'})", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "customer.*", + "account.*", + "tier.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "trading", + "threshold" + ] + }, + { + "slot": "4", + "name": "get-internal-documents", + "display_name": "Get Internal Documents", + "description": "Return internal documents reachable from the caller filtered by taxonomy.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub})-[:WORKS_IN|CAN_ACCESS|CONTAINS*..4]->(document:Document)-[:IS_TYPE]->(taxonomy:Taxonomy {external_id: $taxonomy_external_id})", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "document.*", + "taxonomy.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "documents", + "taxonomy" + ] + }, + { + "slot": "5", + "name": "get-customer-facing-documents", + "display_name": "Get Customer Facing Documents", + "description": "List customer facing documents together with their regulatory agreements and investment categories.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "_Application" + }, + "condition": { + "cypher": "MATCH (subject:_Application {external_id: $_appId}) WITH subject MATCH (doc:Document)-[:SUBJECT_TO]->(ra:RegulatoryAgreement), (doc)-[:CLASSIFIED_AS]->(ic:InvestmentCategory)", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "doc.*", + "ra.*", + "ic.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "documents", + "customer" + ] + }, + { + "slot": "6", + "name": "get-regulatory-agreements", + "display_name": "Get Regulatory Agreements", + "description": "List all regulatory agreements known to the graph.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "_Application" + }, + "condition": { + "cypher": "MATCH (subject:_Application {external_id: $_appId}) WITH subject MATCH (ra:RegulatoryAgreement)", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "ra", + "ra.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "regulatory" + ] + }, + { + "slot": "7", + "name": "get-decisions", + "display_name": "Get Decisions", + "description": "Given a document, return the decisions that referenced it along with the ticket, the customer and the affected account.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub})-[:WORKS_IN|CAN_ACCESS|CONTAINS*..4]->(document:Document {external_id: $document_external_id})<-[document_applied]-(decision:Decision)<--(ticket:Ticket)-[:REGARDING]->(account:Account), (ticket)-[:CREATED_BY]-(customer:Customer), (decision)<-[:MADE]-(actor:User)", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "actor.*", + "document.*", + "decision", + "decision.*", + "ticket.*", + "account.*", + "customer.*" + ], + "relationships": [ + "document_applied.*" + ], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "decisions", + "tickets" + ] + }, + { + "slot": "8", + "name": "get-agent-workflows", + "display_name": "Get Agent Workflows", + "description": "Given an agent get the workflow and all of the possible relationships", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "_Application" + }, + "condition": { + "cypher": "MATCH (subject:_Application) MATCH (wf:Workflow)-[rels:INVOKES*]->(a:Agent {external_id: $agent_id}) WHERE ALL(r IN rels WHERE r.workflow_name = wf.external_id AND endNode(r):Agent) WITH subject, wf.external_id AS workflow, [r IN rels | endNode(r).external_id] AS agent_list", + "filter": [] + }, + "allowed_reads": { + "nodes": [], + "relationships": [], + "aggregate_values": [ + "workflow", + "agent_list" + ] + } + }, + "tags": [] + }, + { + "slot": "9", + "name": "get-hq-weather", + "display_name": "Get HQ Weather", + "description": "Return the current weather reading for CanBank's London headquarters. The hq_weather Weather node's temperature property is populated at query time by the `weather` external data resolver, which calls open-meteo.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub}) WITH subject MATCH (weather:Weather {external_id: 'hq_weather'})", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "subject.*", + "weather.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "canbank", + "weather" + ] + }, + { + "slot": "10", + "name": "store-decision", + "display_name": "Store Decision", + "description": "Create a new Decision node and wire it to the document it applies (APPLIES), the ticket it closes (CLOSED_BY) and the authenticated user who made it (MADE) in one atomic CIQ upsert.", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "User" + }, + "condition": { + "cypher": "MATCH (subject:User {external_id: $token.sub}) MATCH (document:Document {external_id: $document_external_id}) MATCH (ticket:Ticket {external_id: $ticket_external_id})", + "filter": [] + }, + "allowed_upserts": { + "nodes": { + "node_types": [ + "Decision" + ] + }, + "relationships": { + "relationship_types": [ + { + "type": "APPLIES", + "source_node_label": "Decision", + "target_node_label": "Document" + }, + { + "type": "MADE", + "source_node_label": "User", + "target_node_label": "Decision" + }, + { + "type": "CLOSED_BY", + "source_node_label": "Ticket", + "target_node_label": "Decision" + } + ] + } + } + }, + "tags": [ + "canbank", + "decisions", + "write" + ] + }, + { + "slot": "11", + "name": "explain-staff-workflow-access", + "display_name": "Explain Staff Workflow Access", + "description": "Read the User-Department-Workflow authorization path for the console explainer (CanBank).", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "_Application" + }, + "condition": { + "cypher": "MATCH (subject:_Application) MATCH (u:User {external_id: $subject_id})-[w:WORKS_IN]->(d:Department)-[t:CAN_TRIGGER]->(wf:Workflow {external_id: $workflow_id})", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "u.*", + "d.*", + "wf.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "explain", + "authorization" + ] + }, + { + "slot": "12", + "name": "explain-direct-workflow-access", + "display_name": "Explain Direct Workflow Access", + "description": "Read the direct User-Workflow CAN_TRIGGER edge for the console explainer (CanBank).", + "policy": { + "meta": { + "policy_version": "1.0-ciq" + }, + "subject": { + "type": "_Application" + }, + "condition": { + "cypher": "MATCH (subject:_Application) MATCH (u:User {external_id: $subject_id})-[t:CAN_TRIGGER]->(wf:Workflow {external_id: $workflow_id})", + "filter": [] + }, + "allowed_reads": { + "nodes": [ + "u.*", + "wf.*" + ], + "relationships": [], + "aggregate_values": [] + } + }, + "tags": [ + "explain", + "authorization" + ] + } + ], + "resolvers": [ + { + "slot": "1", + "name": "weather", + "display_name": "Current Weather", + "description": "Fetches the current weather block (temperature, apparent temperature, wind speed and weather code) for the latitude/longitude passed via input_params. Defaults to CanBank's London HQ (51.5072, -0.1276) when input_params are absent. Use this as the `external_value` on a Weather node property. Pair with the `weather-units` resolver to also retrieve the unit labels.", + "url": "https://api.open-meteo.com/v1/forecast?latitude={$latitude || 51.5072}&longitude={$longitude || -0.1276}¤t=temperature_2m,apparent_temperature,wind_speed_10m,weather_code&timezone=auto", + "method": "GET", + "headers": {}, + "request_payload": "", + "request_content_type": "JSON", + "response_content_type": "JSON", + "response_selector": ".current" + }, + { + "slot": "2", + "name": "weather-units", + "display_name": "Current Weather Units", + "description": "Fetches the unit labels (e.g. \u00b0C, km/h) for the current weather block at the latitude/longitude passed via input_params (defaults to London HQ).", + "url": "https://api.open-meteo.com/v1/forecast?latitude={$latitude || 51.5072}&longitude={$longitude || -0.1276}¤t=temperature_2m,apparent_temperature,wind_speed_10m,weather_code&timezone=auto", + "method": "GET", + "headers": {}, + "request_payload": "", + "request_content_type": "JSON", + "response_content_type": "JSON", + "response_selector": ".current_units" + }, + { + "slot": "3", + "name": "stock-quote", + "display_name": "Stock Quote", + "description": "Returns the current market price for the ticker symbol passed via the knowledge query's `ticker` input parameter. Bound to the price property of the stock_quote Quote node and consumed by the iag-demo retriever_agent's max-purchase-amount tool. Backed by Yahoo Finance's public chart endpoint; no API key required.", + "url": "https://query1.finance.yahoo.com/v8/finance/chart/{$ticker}?interval=1d", + "method": "GET", + "headers": {}, + "request_payload": "", + "request_content_type": "JSON", + "response_content_type": "JSON", + "response_selector": ".chart.result[0].meta.regularMarketPrice" + } + ], + "app_agent": { + "name": "banking-agent", + "display_name": "Banking Agent", + "description": "App agent for the banking demo", + "api_permissions": [ + "Authorization", + "Capture", + "ContXIQ", + "IKGRead", + "EntityMatching", + "ReadDataSchema" + ], + "credentials_expire_days": 180 + }, + "mcp_server": { + "name": "canbank-mcp-server", + "display_name": "CanBank MCP Server", + "description": "MCP Server configuration for CanBank \u2014 binds the App Agent and Token Introspect used to authenticate inbound MCP traffic.", + "enabled": true, + "scopes_supported": [ + "name", + "email" + ] + }, + "token_introspect": [ + { + "name": "canbank-token-introspect", + "display_name": "CanBank Token Introspect", + "description": "Token introspect for CanBank users authenticated via their IdP", + "ikg_node_type": "User", + "jwt_matcher": { + "issuer": "https://idsvr.indykite.one/oauth/v2/oauth-anonymous/", + "audience": "indykiteagent-mcp" + }, + "claims_mapping": { + "email": { + "selector": "email" + } + }, + "offline_validation": {}, + "perform_upsert": true + }, + { + "name": "canbank-user-token-introspect", + "display_name": "CanBank User Token Introspect", + "description": "Raw user access tokens (aud chatbot-mcp) reaching the MCP endpoint in token-service mode", + "ikg_node_type": "User", + "jwt_matcher": { + "issuer": "https://idsvr.indykite.one/oauth/v2/oauth-anonymous/", + "audience": "chatbot-mcp" + }, + "claims_mapping": { + "email": { + "selector": "email" + } + }, + "offline_validation": {}, + "perform_upsert": true + }, + { + "name": "token-service-https-chatbot-mcp", + "display_name": "Token Service Delegations (chatbot-mcp audience)", + "description": "Token-service-minted delegation JWTs arriving in X-IK-Token; audience inherited from the user subject token (chatbot-mcp). The public JWK comes from .env (TOKEN_SERVICE_PUBLIC_JWK) - fetch it from the running service's /.well-known/jwks.json and strip it to kty/n/e/alg/kid/use.", + "ikg_node_type": "User", + "jwt_matcher": { + "issuer": "https://token-service.demo.local", + "audience": "chatbot-mcp" + }, + "claims_mapping": {}, + "offline_validation": { + "public_jwks": [ + "${TOKEN_SERVICE_PUBLIC_JWK}" + ] + }, + "perform_upsert": true + } + ], + "kbac": [ + { + "name": "user-can-trigger-workflow", + "display_name": "User can trigger workflow", + "description": "Allow a user to trigger an agentic workflow when their department can trigger it OR they have been assigned to it directly.", + "status": "ACTIVE", + "policy": { + "meta": { + "policy_version": "1.0-indykite" + }, + "subject": { + "type": "User" + }, + "actions": [ + "CAN_TRIGGER" + ], + "resource": { + "type": "Workflow" + }, + "condition": { + "cypher": "MATCH (subject:User)-[:WORKS_IN|CAN_TRIGGER*..3]->(resource:Workflow)" + } + } + }, + { + "name": "user-can-retrieve-quote", + "display_name": "User can retrieve stock quote", + "description": "Allow a user to retrieve a stock quote when their department can retrieve it. Note: evaluations carrying a user bearer token (e.g. via the MCP server) are bound to the token's subject - a user can only be evaluated as themselves.", + "status": "ACTIVE", + "policy": { + "meta": { + "policy_version": "2.0-kbac" + }, + "subject": { + "type": "User" + }, + "actions": [ + "CAN_RETRIEVE" + ], + "resource": { + "type": "Quote" + }, + "condition": { + "cypher": "MATCH (subject:User)-[:WORKS_IN]->(department:Department)-[:CAN_RETRIEVE]->(resource:Quote)" + } + } + } + ], + "application": { + "name": "banking-app", + "display_name": "Banking App", + "description": "Banking application that consumes the IndyKite Knowledge Graph" + }, + "project": { + "name": "banking-demo-ts", + "display_name": "Banking Demo TS", + "description": "Banking demo project TS for IndyKite KBAC and ContX IQ", + "region": "europe-west1", + "db_connection": { + "name": "", + "password": "", + "url": "", + "username": "" + } + } +} diff --git a/instant-stack/data/canbank-ts/nodes.json b/instant-stack/data/canbank-ts/nodes.json new file mode 100644 index 0000000..167bc47 --- /dev/null +++ b/instant-stack/data/canbank-ts/nodes.json @@ -0,0 +1,1624 @@ +{ + "nodes": [ + { + "external_id": "millicent", + "type": "User", + "is_identity": true, + "labels": [ + "Employee" + ], + "properties": [ + { + "type": "name", + "value": "Millicent Contextsworth", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "title", + "value": "CSR 2", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "email", + "value": "millicent@canbank.com", + "metadata": { + "source": "Exchange", + "verified_time": "2026-02-20T00:57:13Z", + "assurance_level": 3 + } + } + ] + }, + { + "external_id": "alexb", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Alex B" + } + ] + }, + { + "external_id": "jeff", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Jeff Sanders" + } + ] + }, + { + "external_id": "joe", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Joe Smith" + } + ] + }, + { + "external_id": "jane", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Jane Jacobs" + } + ] + }, + { + "external_id": "carol", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Carol Davis" + } + ] + }, + { + "external_id": "rebecca", + "type": "User", + "is_identity": true, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Rebecca Welton" + } + ] + }, + { + "external_id": "wf1", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf2", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf3", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf-drive", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf-drive-analyst", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf-drive-console", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "wf3-console", + "type": "Workflow", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent-2", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent-3", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent-4", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent-mcp", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "indykiteagent-drive", + "type": "Agent", + "is_identity": false, + "properties": [] + }, + { + "external_id": "s1", + "type": "Skill", + "is_identity": false, + "properties": [] + }, + { + "external_id": "s2", + "type": "Skill", + "is_identity": false, + "properties": [] + }, + { + "external_id": "s3", + "type": "Skill", + "is_identity": false, + "properties": [] + }, + { + "external_id": "t1", + "type": "Tool", + "is_identity": false, + "properties": [] + }, + { + "external_id": "t2", + "type": "Tool", + "is_identity": false, + "properties": [] + }, + { + "external_id": "canbank", + "type": "Entity", + "is_identity": false, + "tags": [ + "Organization" + ], + "properties": [ + { + "type": "name", + "value": "CanBank" + } + ] + }, + { + "external_id": "canbank-depts", + "type": "Container", + "is_identity": false, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Departments" + } + ] + }, + { + "external_id": "exec", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Executive", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "lob", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Lines of Business", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "governance", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Governance", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "shared", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Shared Services", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "support", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Customer Support", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "consumer", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Consumer Banking", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "commercial", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Commercial Banking", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "private", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Private Banking", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "trading", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Trading", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "risk", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Risk Management", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "crimes", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Financial Crimes Compliance", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "regulatory", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Regulatory Affairs", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "treasury", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Treasury & Asset-Liability Management", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "tech", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Global Tech", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "ops", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Business Operations", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "strategy", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Corporate Strategy", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "procurement", + "type": "Department", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Strategic Sourcing", + "metadata": { + "source": "Workday" + } + } + ] + }, + { + "external_id": "leslie", + "type": "User", + "is_identity": true, + "labels": [ + "Employee" + ], + "properties": [ + { + "type": "name", + "value": "Leslie Higgins", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "title", + "value": "CSR 2", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "email", + "value": "leslie@canbank.com", + "metadata": { + "source": "Exchange", + "verified_time": "2026-02-20T00:57:13Z", + "assurance_level": 3 + } + } + ] + }, + { + "external_id": "flo", + "type": "User", + "is_identity": true, + "labels": [ + "Employee" + ], + "properties": [ + { + "type": "name", + "value": "Flo Collins", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "title", + "value": "Customer Service Supervisor", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "email", + "value": "flo@canbank.com", + "metadata": { + "source": "Exchange", + "verified_time": "2026-02-20T00:57:13Z", + "assurance_level": 3 + } + } + ] + }, + { + "external_id": "roy", + "type": "User", + "is_identity": true, + "labels": [ + "Employee" + ], + "properties": [ + { + "type": "name", + "value": "Roy Kent", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "title", + "value": "Trader", + "metadata": { + "source": "Workday", + "verified_time": "2026-02-26T00:57:13Z", + "assurance_level": 3 + } + }, + { + "type": "email", + "value": "roy@canbank.com", + "metadata": { + "source": "Exchange", + "verified_time": "2026-02-20T00:57:13Z", + "assurance_level": 3 + } + } + ] + }, + { + "external_id": "stock_quote", + "type": "Quote", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "price", + "external_value": "stock-quote" + } + ] + }, + { + "external_id": "cs_policy", + "type": "Folder", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Customer Service Policy", + "metadata": { + "source": "Sharepoint" + } + } + ] + }, + { + "external_id": "refund_policy", + "type": "Document", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Customer Refund Policy", + "metadata": { + "source": "Sharepoint" + } + }, + { + "type": "description", + "value": "Clear criteria for when a refund is authorized, who has the power to grant it, and what documentation is required. Tier 4 customers can have their credit card late payment penalties waived without consulting a supervisor." + }, + { + "type": "url", + "value": "https://sharepoint.miscrosoft.com/canbank/site/csr/refund_policy.pdf", + "metadata": { + "source": "Sharepoint" + } + } + ] + }, + { + "external_id": "discount_policy", + "type": "Document", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Discount Policy", + "metadata": { + "source": "Sharepoint" + } + }, + { + "type": "description", + "value": "Guidelines on what \"goodwill\" gestures an agent can offer to resolve a complaint without needing managerial approval." + }, + { + "type": "url", + "value": "https://sharepoint.miscrosoft.com/canbank/site/csr/discount_policy.pdf", + "metadata": { + "source": "Sharepoint" + } + } + ] + }, + { + "external_id": "kyc_policy", + "type": "Document", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "KYC Policy", + "metadata": { + "source": "Sharepoint" + } + }, + { + "type": "description", + "value": "The exact steps an agent must take to verify a customer\u2019s identity before proceeding to assist them." + }, + { + "type": "url", + "value": "https://sharepoint.miscrosoft.com/canbank/site/csr/kyc_policy.pdf", + "metadata": { + "source": "Sharepoint" + } + } + ] + }, + { + "external_id": "policy", + "type": "Taxonomy", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Policy" + }, + { + "type": "description", + "value": "Formal governing documents that define the rules, standards, and principles of the organization. These documents mandate specific conduct to ensure legal compliance, mitigate risk, and align operations with our core mission and values." + } + ] + }, + { + "external_id": "ticket_001", + "type": "Ticket", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Credit card late payment penalty" + }, + { + "type": "description", + "value": "I was only a few hours late with my credit card payment but I still received the fine. I would like the fine reversed." + } + ] + }, + { + "external_id": "ticket_002", + "type": "Ticket", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Credit card late payment penalty" + }, + { + "type": "description", + "value": "I was only a few hours late with my credit card payment but I still received the fine. I would like the fine reversed." + } + ] + }, + { + "external_id": "decision_001", + "type": "Decision", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "name", + "value": "Waive Credit Card Late Payment Charge", + "metadata": { + "source": "Salesforce" + } + }, + { + "type": "description", + "value": "Flo Collins, CSR 2, waived the late payment change for Ted Lasso because Ted was a loyal customer and did not have any other late payment chanrges. Flo was concerned that Ted was at risk of leaving CanBank over the late credit card payment charge.", + "metadata": { + "source": "Salesforce" + } + } + ] + }, + { + "external_id": "customer", + "type": "Site", + "is_identity": false, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Customer" + } + ] + }, + { + "external_id": "docs", + "type": "Folder", + "is_identity": false, + "tags": [], + "properties": [ + { + "type": "name", + "value": "Documents" + } + ] + }, + { + "external_id": "jur-canada", + "type": "Jurisdiction", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Canada" + }, + { + "type": "iso_3166_2", + "value": "CA" + } + ] + }, + { + "external_id": "jur-usa", + "type": "Jurisdiction", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "USA" + }, + { + "type": "iso_3166_2", + "value": "US" + } + ] + }, + { + "external_id": "jur-emea", + "type": "Jurisdiction", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "EMEA" + } + ] + }, + { + "external_id": "jur-switzerland", + "type": "Jurisdiction", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Switzerland" + } + ] + }, + { + "external_id": "cat-mutual-fund", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Mutual Fund" + } + ] + }, + { + "external_id": "cat-etf", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Exchange Traded Fund" + } + ] + }, + { + "external_id": "cat-fixed-income", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Fixed Income" + } + ] + }, + { + "external_id": "cat-derivatives", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Derivatives" + } + ] + }, + { + "external_id": "cat-private-equity", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Private Equity" + } + ] + }, + { + "external_id": "cat-islamic-finance", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Sukuk" + } + ] + }, + { + "external_id": "cat-cash", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Guaranteed Investment" + } + ] + }, + { + "external_id": "cat-lending", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Lombard Lending" + } + ] + }, + { + "external_id": "cat_stocks", + "type": "InvestmentCategory", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Stocks" + } + ] + }, + { + "external_id": "reg-csa-ni81", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CSA NI 81-101" + }, + { + "type": "description", + "value": "Compliance with National Instrument 81-101 (NI 81-101), including the preparation, filing, and delivery of simplified prospectuses, annual information forms (AIFs), and fund facts documents to investors in accordance with Canadian securities legislation." + } + ] + }, + { + "external_id": "reg-cdic", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CDIC Act" + }, + { + "type": "description", + "value": "An agreement between [Institution Name] and the Canada Deposit Insurance Corporation ('CDIC'), intended to ensure ongoing compliance with the Canada Deposit Insurance Corporation Act and its associated By-laws, specifically regarding the proper identification of insured deposits, timely reporting of financial data, and adherence to sound business and financial practices as required under the Act." + } + ] + }, + { + "external_id": "reg-sec-40", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "SEC 1940 Act" + }, + { + "type": "description", + "value": "This Agreement is entered into to ensure that [Fund Name] (the 'Fund') operates in compliance with the applicable provisions of the Investment Company Act of 1940, as amended (the '1940 Act'), and the rules thereunder. The parties agree to adhere to all requirements regarding the registration, valuation of portfolio securities, custody of assets, and limitations on transactions with affiliates. This agreement ensures the Fund maintains its status as a registered investment company or properly relies on a specific exemption." + } + ] + }, + { + "external_id": "reg-sec-498", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "SEC Rule 498" + }, + { + "type": "description", + "value": "Compliant with SEC Rule 498, allowing for the delivery of a 'Summary Prospectus' for open-end mutual funds. Requires that key investor information be provided upfront, while detailed documents are available online. Ensures compliance with 'access equals delivery' provisions, provided that the required documents are hosted on a publicly accessible website, properly hyperlinked, and that requests for paper copies are met within three business days." + } + ] + }, + { + "external_id": "reg-ucits-v", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "UCITS V" + }, + { + "type": "description", + "value": "Under this agreement, the Depositary assumes strict liability for the loss of financial instruments held in custody, maintains legally protected segregation of assets (including at the sub-custodian level), and ensures that all delegation of safekeeping functions meets the stringent due diligence and independence standards of UCITS V." + } + ] + }, + { + "external_id": "reg-priips", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "PRIIPS Regulation" + }, + { + "type": "description", + "value": "The firm complies with Regulation (EU) No 1286/2014 (and the UK equivalent, where applicable) regarding Packaged Retail and Insurance-based Investment Products (PRIIPs). We ensure that a Key Information Document (KID) is produced for all in-scope products, is accurate, fair, clear, and not misleading, and that it is provided to retail investors in good time prior to their investment. The KID is reviewed annually and updated promptly upon any significant change to risk, cost, or performance." + } + ] + }, + { + "external_id": "reg-isda", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "ISDA Master" + }, + { + "type": "description", + "value": "An industry-standardized, bilateral contract governing over-the-counter (OTC) derivatives, designed to enforce close-out netting, mitigate credit risk, and provide a compliant framework for regulatory reporting and margin exchange (e.g., EMIR, Dodd-Frank)." + } + ] + }, + { + "external_id": "reg-ni45", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "NI 45-106" + }, + { + "type": "description", + "value": "The issuance of Securities is being made in reliance upon exemptions from the prospectus requirements of applicable Canadian provincial and territorial securities legislation, specifically in accordance with the exemptions set out in National Instrument 45-106 \u2013 Prospectus Exemptions ('NI 45-106'). The Issuer represents that no prospectus has been filed, and therefore no statutory rights of action or rescission exist, except as otherwise provided by applicable law or the terms of an Offering Memorandum." + } + ] + }, + { + "external_id": "reg-fidleg", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Swiss FIDLEG" + }, + { + "type": "description", + "value": "This Agreement is subject to the Swiss Financial Services Act (FIDLEG/FinSA), which entered into force on January 1, 2020. [Company Name] acts as a financial service provider in accordance with FIDLEG. Based on the client information provided, the Client is classified as [Retail/Professional/Institutional]. In accordance with the Rules of Conduct (Art. 7-20 FIDLEG), [Company Name] is committed to acting in the client's best interest, providing transparency regarding products (including Key Information Documents - KIDs), disclosing fees/retrocessions, and ensuring best execution of orders. A [Suitability/Appropriateness] test will be performed. The client is aware of the risks associated with financial instruments as detailed in the 'Risks Involved in Trading Financial Instruments' brochure. This agreement ensures compliance with documentation and accountability requirements." + } + ] + }, + { + "external_id": "reg-emir", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "EU EMIR" + }, + { + "type": "description", + "value": "This Agreement governs the compliance of [Party A] and [Party B] with the European Market Infrastructure Regulation (EU) No 648/2012 as amended (EMIR/EMIR Refit). It encompasses all applicable obligations, including the reporting of OTC and Exchange Traded Derivatives (ETD) to an authorized Trade Repository (Article 9), the central clearing of eligible OTC derivatives (Article 4), and the application of Risk Mitigation Techniques (including timely confirmation, portfolio reconciliation, dispute resolution, and margin exchange for non-cleared trades)." + } + ] + }, + { + "external_id": "reg-uae-cb", + "type": "RegulatoryAgreement", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "New CBUAE Law" + }, + { + "type": "description", + "value": "Law No. (6) of 2025 (the \"New CBUAE Law\") is a landmark overhaul that consolidates the UAE's financial regulatory framework, unifying the oversight of banking, insurance, and payment systems under the Central Bank of the UAE (CBUAE). It effectively repeals the previous 2018 Central Bank Law and the 2023 Insurance Law." + } + ] + }, + { + "external_id": "doc-001", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank Maple Balanced Fund Facts" + } + ] + }, + { + "external_id": "doc-002", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "5-Year GIC Disclosure Statement" + } + ] + }, + { + "external_id": "doc-003", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank Blue-Chip Growth Prospectus" + } + ] + }, + { + "external_id": "doc-004", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank S&P 500 ETF Summary Prospectus" + } + ] + }, + { + "external_id": "doc-005", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank Euro-Agg UCITS KIID" + } + ] + }, + { + "external_id": "doc-006", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Sustainable EMEA Equity KID" + } + ] + }, + { + "external_id": "doc-007", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Global Multi-Asset Term Sheet" + } + ] + }, + { + "external_id": "doc-008", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank Treasury Bond Offering Circular" + } + ] + }, + { + "external_id": "doc-009", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Private Wealth Equity Pool OM" + } + ] + }, + { + "external_id": "doc-010", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Lombard Loan Facility Agreement" + } + ] + }, + { + "external_id": "doc-011", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Gulf Opportunities Sukuk Prospectus" + } + ] + }, + { + "external_id": "doc-012", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "US High-Yield Debt Wrapper" + } + ] + }, + { + "external_id": "doc-013", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Venture Tech LP Partnership Deed" + } + ] + }, + { + "external_id": "doc-014", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Swiss Wealth Mgmt Addendum" + } + ] + }, + { + "external_id": "doc-015", + "type": "Document", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Cross-Asset Hedging Disclosure" + } + ] + }, + { + "external_id": "sys_canbank_core", + "type": "System", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "CanBank Core Banking Retail" + } + ] + }, + { + "external_id": "alison", + "type": "Customer", + "is_identity": true, + "properties": [ + { + "type": "name", + "value": "Alice Martin", + "metadata": { + "source": "RetailCore" + } + }, + { + "type": "email", + "value": "alison@example.ca" + } + ] + }, + { + "external_id": "bob", + "type": "Customer", + "is_identity": true, + "properties": [ + { + "type": "name", + "value": "Bob Vance", + "metadata": { + "source": "RetailCore" + } + }, + { + "type": "email", + "value": "bob@vancefridge.com" + } + ] + }, + { + "external_id": "charlie", + "type": "Customer", + "is_identity": true, + "properties": [ + { + "type": "name", + "value": "Charlie Day", + "metadata": { + "source": "RetailCore" + } + }, + { + "type": "email", + "value": "ratking@philly.com" + } + ] + }, + { + "external_id": "rebecca", + "type": "Customer", + "is_identity": true, + "properties": [ + { + "type": "name", + "value": "Rebecca Welton", + "metadata": { + "source": "Core" + } + }, + { + "type": "email", + "value": "rebeccaw@afcrichmond.com" + } + ] + }, + { + "external_id": "ted", + "type": "Customer", + "is_identity": true, + "properties": [ + { + "type": "name", + "value": "Ted Lasso", + "metadata": { + "source": "RetailCore" + } + }, + { + "type": "email", + "value": "tedl@afcrichmond.com" + } + ] + }, + { + "external_id": "acc_alison_chequing", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Chequing" + }, + { + "type": "balance", + "value": "5200.00" + } + ] + }, + { + "external_id": "acc_alison_mortgage", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Mortgage" + }, + { + "type": "interest_rate", + "value": "4.5%" + } + ] + }, + { + "external_id": "acc_bob_401k", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "401k" + }, + { + "type": "provider", + "value": "CanBank USA" + } + ] + }, + { + "external_id": "acc_charlie_loan", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Personal Loan" + }, + { + "type": "status", + "value": "Active" + } + ] + }, + { + "external_id": "acc_rebecca_chequing", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Chequing" + }, + { + "type": "status", + "value": "Active" + } + ] + }, + { + "external_id": "acc_rebecca_investing", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Investing" + }, + { + "type": "status", + "value": "Active" + }, + { + "type": "holdings", + "value": "Active" + } + ] + }, + { + "external_id": "acc_ted_investing", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "Investing" + }, + { + "type": "status", + "value": "Active" + }, + { + "type": "holdings", + "value": "Active" + } + ] + }, + { + "external_id": "acc_ted_cc", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "CreditCard" + }, + { + "type": "status", + "value": "paid" + } + ] + }, + { + "external_id": "acc_rebecca_cc", + "type": "Account", + "is_identity": false, + "properties": [ + { + "type": "account_type", + "value": "CreditCard" + }, + { + "type": "status", + "value": "late" + } + ] + }, + { + "external_id": "tier_1", + "type": "AccountTier", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Tier 1" + }, + { + "type": "tier", + "value": 1 + }, + { + "type": "threshold_amount", + "value": 1000 + } + ] + }, + { + "external_id": "tier_2", + "type": "AccountTier", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Tier 2" + }, + { + "type": "tier", + "value": 2 + }, + { + "type": "threshold_amount", + "value": 5000 + } + ] + }, + { + "external_id": "tier_3", + "type": "AccountTier", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Tier 3" + }, + { + "type": "tier", + "value": 3 + }, + { + "type": "threshold_amount", + "value": 10000 + } + ] + }, + { + "external_id": "tier_4", + "type": "AccountTier", + "is_identity": false, + "properties": [ + { + "type": "name", + "value": "Tier 4" + }, + { + "type": "tier", + "value": 4 + }, + { + "type": "threshold_amount", + "value": 1000000 + } + ] + }, + { + "external_id": "hq_weather", + "type": "Weather", + "is_identity": false, + "labels": [], + "properties": [ + { + "type": "location", + "value": "London" + }, + { + "type": "latitude", + "value": 51.5072 + }, + { + "type": "longitude", + "value": -0.1276 + }, + { + "type": "current", + "external_value": "weather" + }, + { + "type": "units", + "external_value": "weather-units" + } + ] + } + ] +} diff --git a/instant-stack/data/canbank-ts/relationships.json b/instant-stack/data/canbank-ts/relationships.json new file mode 100644 index 0000000..0904347 --- /dev/null +++ b/instant-stack/data/canbank-ts/relationships.json @@ -0,0 +1,1987 @@ +{ + "relationships": [ + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Department", + "external_id": "support" + }, + "properties": [], + "type": "WORKS_IN" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Department", + "external_id": "trading" + }, + "properties": [], + "type": "WORKS_IN" + }, + { + "source": { + "type": "User", + "external_id": "alexb" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "jeff" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "joe" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "carol" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "jane" + }, + "target": { + "type": "Workflow", + "external_id": "wf2" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf3" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "alexb" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "jeff" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "joe" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "carol" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "jane" + }, + "target": { + "type": "Workflow", + "external_id": "wf2" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf3" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "rebecca" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "rebecca" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Department", + "external_id": "support" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Department", + "external_id": "support" + }, + "target": { + "type": "Workflow", + "external_id": "wf2" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Department", + "external_id": "trading" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf1" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf1" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-2" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf1" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-2" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-mcp" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf1" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf2" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf2" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-3" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf2" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-3" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-mcp" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf2" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf3" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf3" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf-drive" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-drive" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-mcp" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf3" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive-analyst" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive-analyst" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive-console" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf-drive-console" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf-drive-analyst" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive-analyst" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-drive" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive-analyst" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf-drive-console" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive-console" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive-console" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-drive" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf-drive-console" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf3-console" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "millicent" + }, + "target": { + "type": "Workflow", + "external_id": "wf3-console" + }, + "properties": [], + "type": "CAN_TRIGGER" + }, + { + "source": { + "type": "Workflow", + "external_id": "wf3-console" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf3-console" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf3-console" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-4" + }, + "target": { + "type": "Agent", + "external_id": "indykiteagent-mcp" + }, + "properties": [ + { + "type": "workflow_name", + "value": "wf3-console" + }, + { + "type": "discriminating_property", + "value": "workflow_name" + } + ], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Skill", + "external_id": "s1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent" + }, + "target": { + "type": "Skill", + "external_id": "s2" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Skill", + "external_id": "s2" + }, + "target": { + "type": "Tool", + "external_id": "t1" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Skill", + "external_id": "s2" + }, + "target": { + "type": "Tool", + "external_id": "t2" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Agent", + "external_id": "indykiteagent-2" + }, + "target": { + "type": "Skill", + "external_id": "s3" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Skill", + "external_id": "s3" + }, + "target": { + "type": "Tool", + "external_id": "t2" + }, + "properties": [], + "type": "INVOKES" + }, + { + "source": { + "type": "Entity", + "external_id": "canbank" + }, + "target": { + "type": "Container", + "external_id": "canbank-depts" + }, + "properties": [], + "type": "CONTAINS" + }, + { + "source": { + "type": "Container", + "external_id": "canbank-depts" + }, + "target": { + "type": "Department", + "external_id": "exec" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "exec" + }, + "target": { + "type": "Department", + "external_id": "lob" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "exec" + }, + "target": { + "type": "Department", + "external_id": "governance" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "exec" + }, + "target": { + "type": "Department", + "external_id": "shared" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "governance" + }, + "target": { + "type": "Department", + "external_id": "risk" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "governance" + }, + "target": { + "type": "Department", + "external_id": "crimes" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "governance" + }, + "target": { + "type": "Department", + "external_id": "regulatory" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "governance" + }, + "target": { + "type": "Department", + "external_id": "treasury" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "shared" + }, + "target": { + "type": "Department", + "external_id": "tech" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "shared" + }, + "target": { + "type": "Department", + "external_id": "ops" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "shared" + }, + "target": { + "type": "Department", + "external_id": "strategy" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "shared" + }, + "target": { + "type": "Department", + "external_id": "procurement" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Department", + "external_id": "support" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "type": "INVOKES" + }, + { + "source": { + "type": "Department", + "external_id": "trading" + }, + "target": { + "type": "Workflow", + "external_id": "wf1" + }, + "type": "INVOKES" + }, + { + "source": { + "type": "User", + "external_id": "leslie" + }, + "target": { + "type": "Department", + "external_id": "support" + }, + "type": "WORKS_IN" + }, + { + "source": { + "type": "User", + "external_id": "flo" + }, + "target": { + "type": "Department", + "external_id": "support" + }, + "type": "WORKS_IN" + }, + { + "source": { + "type": "User", + "external_id": "roy" + }, + "target": { + "type": "Department", + "external_id": "trading" + }, + "type": "WORKS_IN" + }, + { + "source": { + "type": "User", + "external_id": "leslie" + }, + "target": { + "type": "User", + "external_id": "flo" + }, + "type": "REPORTS_TO" + }, + { + "source": { + "type": "Department", + "external_id": "trading" + }, + "target": { + "type": "Quote", + "external_id": "stock_quote" + }, + "type": "CAN_RETRIEVE" + }, + { + "source": { + "type": "Department", + "external_id": "trading" + }, + "target": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "type": "CAN_ACCESS" + }, + { + "source": { + "type": "Department", + "external_id": "support" + }, + "target": { + "type": "Folder", + "external_id": "cs_policy" + }, + "type": "CAN_ACCESS" + }, + { + "source": { + "type": "Folder", + "external_id": "cs_policy" + }, + "target": { + "type": "Document", + "external_id": "refund_policy" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "cs_policy" + }, + "target": { + "type": "Document", + "external_id": "discount_policy" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "cs_policy" + }, + "target": { + "type": "Document", + "external_id": "kyc_policy" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Document", + "external_id": "refund_policy" + }, + "target": { + "type": "Taxonomy", + "external_id": "policy" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Document", + "external_id": "discount_policy" + }, + "target": { + "type": "Taxonomy", + "external_id": "policy" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Document", + "external_id": "kyc_policy" + }, + "target": { + "type": "Taxonomy", + "external_id": "policy" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Ticket", + "external_id": "ticket_001" + }, + "target": { + "type": "Customer", + "external_id": "ted" + }, + "type": "CREATED_BY" + }, + { + "source": { + "type": "Ticket", + "external_id": "ticket_002" + }, + "target": { + "type": "Customer", + "external_id": "rebecca" + }, + "type": "CREATED_BY" + }, + { + "source": { + "type": "Ticket", + "external_id": "ticket_001" + }, + "target": { + "type": "Account", + "external_id": "acc_ted_cc" + }, + "type": "REGARDING" + }, + { + "source": { + "type": "Ticket", + "external_id": "ticket_002" + }, + "target": { + "type": "Account", + "external_id": "acc_rebecca_cc" + }, + "type": "REGARDING" + }, + { + "source": { + "type": "Ticket", + "external_id": "ticket_001" + }, + "target": { + "type": "Decision", + "external_id": "decision_001" + }, + "type": "CLOSED_BY" + }, + { + "source": { + "type": "Decision", + "external_id": "decision_001" + }, + "target": { + "type": "Document", + "external_id": "refund_policy" + }, + "type": "EXCEPTION_TO" + }, + { + "source": { + "type": "User", + "external_id": "flo" + }, + "target": { + "type": "Decision", + "external_id": "decision_001" + }, + "type": "MADE" + }, + { + "source": { + "type": "Site", + "external_id": "customer" + }, + "target": { + "type": "Folder", + "external_id": "docs" + }, + "properties": [], + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-etf" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-fixed-income" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-derivatives" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-private-equity" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-islamic-finance" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-cash" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Folder", + "external_id": "docs" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-lending" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-001" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-001" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-csa-ni81" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-002" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-cash" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-002" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-cdic" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-013" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-private-equity" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-013" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-ni45" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-003" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-003" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-sec-40" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-004" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-etf" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-004" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-sec-498" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-012" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-fixed-income" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-012" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-isda" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-005" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-005" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-ucits-v" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-006" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-006" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-priips" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-010" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-lending" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-010" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-priips" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-011" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-islamic-finance" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-011" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-uae-cb" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-014" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-fidleg" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-014" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-014" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-fixed-income" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-007" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-fixed-income" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-007" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-derivatives" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-007" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-isda" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-008" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-fixed-income" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-008" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-isda" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-009" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-private-equity" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-009" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-ni45" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-009" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-sec-40" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-015" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-derivatives" + }, + "type": "CLASSIFIED_AS" + }, + { + "source": { + "type": "Document", + "external_id": "doc-015" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-emir" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "Document", + "external_id": "doc-015" + }, + "target": { + "type": "RegulatoryAgreement", + "external_id": "reg-isda" + }, + "type": "SUBJECT_TO" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-csa-ni81" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-canada" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-cdic" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-canada" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-ni45" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-canada" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-sec-40" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-usa" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-sec-498" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-usa" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-ucits-v" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-emea" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-priips" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-emea" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-uae-cb" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-emea" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-emir" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-emea" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "RegulatoryAgreement", + "external_id": "reg-fidleg" + }, + "target": { + "type": "Jurisdiction", + "external_id": "jur-switzerland" + }, + "type": "ENFORCED_IN" + }, + { + "source": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "target": { + "type": "Customer", + "external_id": "alison" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "target": { + "type": "Customer", + "external_id": "bob" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "target": { + "type": "Customer", + "external_id": "charlie" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "target": { + "type": "Customer", + "external_id": "rebecca" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "System", + "external_id": "sys_canbank_core" + }, + "target": { + "type": "Customer", + "external_id": "ted" + }, + "type": "CONTAINS" + }, + { + "source": { + "type": "Customer", + "external_id": "alison" + }, + "target": { + "type": "Account", + "external_id": "acc_alison_chequing" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "alison" + }, + "target": { + "type": "Account", + "external_id": "acc_alison_mortgage" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "bob" + }, + "target": { + "type": "Account", + "external_id": "acc_bob_401k" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "charlie" + }, + "target": { + "type": "Account", + "external_id": "acc_charlie_loan" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "rebecca" + }, + "target": { + "type": "Account", + "external_id": "acc_rebecca_chequing" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "rebecca" + }, + "target": { + "type": "Account", + "external_id": "acc_rebecca_investing" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "rebecca" + }, + "target": { + "type": "Account", + "external_id": "acc_rebecca_cc" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "ted" + }, + "target": { + "type": "Account", + "external_id": "acc_ted_investing" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Customer", + "external_id": "ted" + }, + "target": { + "type": "Account", + "external_id": "acc_ted_cc" + }, + "type": "OWNS" + }, + { + "source": { + "type": "Account", + "external_id": "acc_charlie_loan" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-lending" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Account", + "external_id": "acc_bob_401k" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat-mutual-fund" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Account", + "external_id": "acc_rebecca_investing" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat_stocks" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Account", + "external_id": "acc_ted_investing" + }, + "target": { + "type": "InvestmentCategory", + "external_id": "cat_stocks" + }, + "type": "IS_TYPE" + }, + { + "source": { + "type": "Account", + "external_id": "acc_charlie_loan" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_1" + }, + "type": "IS_TIER" + }, + { + "source": { + "type": "Account", + "external_id": "acc_bob_401k" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_1" + }, + "type": "IS_TIER" + }, + { + "source": { + "type": "Account", + "external_id": "acc_rebecca_investing" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_3" + }, + "type": "IS_TIER" + }, + { + "source": { + "type": "Account", + "external_id": "acc_ted_investing" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_3" + }, + "type": "IS_TIER" + }, + { + "source": { + "type": "Account", + "external_id": "acc_rebecca_cc" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_3" + }, + "type": "IS_TIER" + }, + { + "source": { + "type": "Account", + "external_id": "acc_ted_cc" + }, + "target": { + "type": "AccountTier", + "external_id": "tier_3" + }, + "type": "IS_TIER" + } + ] +} diff --git a/instant-stack/templates/token_introspect/create_form.html b/instant-stack/templates/token_introspect/create_form.html index 2008e86..18c8284 100644 --- a/instant-stack/templates/token_introspect/create_form.html +++ b/instant-stack/templates/token_introspect/create_form.html @@ -10,7 +10,25 @@

Create Token Introspect

Configure token introspection settings and click "Create" to submit.

+ {% if configs and configs | length > 1 %} +
+

This dataset declares {{ configs | length }} token-introspect configs. Create each one; its ID is saved under its own .env entry (shown on the tab).

+ +
+ {% endif %} +
+